changeset 2884:497a637366b4 db-changes

integration mainline->db-changes
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 12 Oct 2018 15:18:10 +0200
parents 2b91363cc1d1 (current diff) 320a877a1f40 (diff)
children ce310baccda6
files Core/ICommand.h Core/MultiThreading/ILockable.h Core/MultiThreading/Locker.h Core/MultiThreading/Mutex.cpp Core/MultiThreading/Mutex.h Core/MultiThreading/ReaderWriterLock.cpp Core/MultiThreading/ReaderWriterLock.h Core/MultiThreading/Semaphore.cpp Core/MultiThreading/Semaphore.h Core/Uuid.cpp Core/Uuid.h OrthancServer/DatabaseWrapperBase.cpp OrthancServer/DatabaseWrapperBase.h OrthancServer/DicomDirWriter.cpp OrthancServer/DicomDirWriter.h OrthancServer/DicomModification.cpp OrthancServer/DicomModification.h OrthancServer/DicomProtocol/DicomFindAnswers.cpp OrthancServer/DicomProtocol/DicomFindAnswers.h OrthancServer/DicomProtocol/DicomServer.cpp OrthancServer/DicomProtocol/DicomServer.h OrthancServer/DicomProtocol/DicomUserConnection.cpp OrthancServer/DicomProtocol/DicomUserConnection.h OrthancServer/DicomProtocol/IApplicationEntityFilter.h OrthancServer/DicomProtocol/IFindRequestHandler.h OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h OrthancServer/DicomProtocol/IMoveRequestHandler.h OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h OrthancServer/DicomProtocol/IStoreRequestHandler.h OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h OrthancServer/DicomProtocol/RemoteModalityParameters.cpp OrthancServer/DicomProtocol/RemoteModalityParameters.h OrthancServer/DicomProtocol/ReusableDicomUserConnection.cpp OrthancServer/DicomProtocol/ReusableDicomUserConnection.h OrthancServer/FromDcmtkBridge.cpp OrthancServer/FromDcmtkBridge.h OrthancServer/Internals/CommandDispatcher.cpp OrthancServer/Internals/CommandDispatcher.h OrthancServer/Internals/DicomImageDecoder.cpp OrthancServer/Internals/DicomImageDecoder.h OrthancServer/Internals/FindScp.cpp OrthancServer/Internals/FindScp.h OrthancServer/Internals/MoveScp.cpp OrthancServer/Internals/MoveScp.h OrthancServer/Internals/StoreScp.cpp OrthancServer/Internals/StoreScp.h OrthancServer/OrthancPeerParameters.cpp OrthancServer/OrthancPeerParameters.h OrthancServer/ParsedDicomFile.cpp OrthancServer/ParsedDicomFile.h OrthancServer/Scheduler/CallSystemCommand.cpp OrthancServer/Scheduler/CallSystemCommand.h OrthancServer/Scheduler/DeleteInstanceCommand.cpp OrthancServer/Scheduler/DeleteInstanceCommand.h OrthancServer/Scheduler/IServerCommand.h OrthancServer/Scheduler/ModifyInstanceCommand.cpp OrthancServer/Scheduler/ModifyInstanceCommand.h OrthancServer/Scheduler/ServerCommandInstance.cpp OrthancServer/Scheduler/ServerCommandInstance.h OrthancServer/Scheduler/ServerJob.cpp OrthancServer/Scheduler/ServerJob.h OrthancServer/Scheduler/ServerScheduler.cpp OrthancServer/Scheduler/ServerScheduler.h OrthancServer/Scheduler/StorePeerCommand.cpp OrthancServer/Scheduler/StorePeerCommand.h OrthancServer/Scheduler/StoreScuCommand.cpp OrthancServer/Scheduler/StoreScuCommand.h OrthancServer/ToDcmtkBridge.cpp OrthancServer/ToDcmtkBridge.h Plugins/Engine/SharedLibrary.cpp Plugins/Engine/SharedLibrary.h Plugins/Include/orthanc/OrthancCppDatabasePlugin.h Plugins/Samples/DatabasePlugin/CMakeLists.txt Plugins/Samples/DatabasePlugin/Database.cpp Plugins/Samples/DatabasePlugin/Database.h Plugins/Samples/DatabasePlugin/Plugin.cpp Plugins/Samples/GdcmDecoding/CMakeLists.txt Plugins/Samples/GdcmDecoding/OrthancContext.cpp Plugins/Samples/GdcmDecoding/OrthancContext.h Plugins/Samples/GdcmDecoding/Plugin.cpp Resources/CMake/GoogleLogConfiguration.cmake Resources/CMake/GoogleLogConfiguration.h Resources/CMake/GoogleLogConfigurationDarwin.h Resources/CMake/GoogleLogConfigurationLSB.h Resources/Patches/dcmtk-linux-speed.patch Resources/Patches/dcmtk-mingw64.patch Resources/Patches/dcmtk-mingw64.txt Resources/Patches/glog-port-cc.diff Resources/Patches/glog-port-h-v2.diff Resources/Patches/glog-port-h.diff Resources/Patches/glog-utilities-lsb.diff Resources/Patches/glog-utilities.diff Resources/Patches/glog-visual-studio-port.h Resources/Patches/log4cplus-patch.diff Resources/Patches/openssl-lsb.diff THANKS
diffstat 668 files changed, 77194 insertions(+), 27485 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,5 @@
+syntax: glob
+ThirdPartyDownloads/
+CMakeLists.txt.user
+*.cpp.orig
+*.h.orig
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgtags	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,1 @@
+a95beca72e99f3a1110cffd252bcf3abf5a2db27 dcmtk-3.6.1
--- a/.travis.yml	Thu Oct 29 11:25:45 2015 +0100
+++ b/.travis.yml	Fri Oct 12 15:18:10 2018 +0200
@@ -31,20 +31,24 @@
     -qq build-essential unzip cmake mercurial uuid-dev libcurl4-openssl-dev liblua5.1-0-dev
     libgtest-dev libpng-dev libsqlite3-dev libssl-dev zlib1g-dev libdcmtk2-dev libwrap0-dev
     libcharls-dev; fi
+  # For DCMTK 3.6.2 - Can't make it compile in static mode with MinGW32 on the
+  # Ubuntu Precise (12.04) that is used by Travis:
+  # - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then sudo apt-get install mingw-w64 gcc-mingw-w64-i686 g++-mingw-w64-i686 wine; fi
+
+  # For DCMTK 3.6.0: 
   - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then sudo apt-get install mingw32; fi
 
-
 before_script:
   - mkdir Build
   - cd Build
   - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == OFF ]; then cmake
     -DCMAKE_BUILD_TYPE=Debug "-DDCMTK_LIBRARIES=CharLS;dcmjpls;wrap;oflog"
     -DALLOW_DOWNLOADS=ON -DUSE_SYSTEM_BOOST=OFF -DUSE_SYSTEM_MONGOOSE=OFF -DUSE_SYSTEM_JSONCPP=OFF
-    -DUSE_SYSTEM_GOOGLE_LOG=OFF -DUSE_SYSTEM_PUGIXML=OFF -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON
+    -DUSE_SYSTEM_GOOGLE_LOG=OFF -DUSE_SYSTEM_PUGIXML=OFF -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON
     ..; fi
   - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then cmake
     -DCMAKE_BUILD_TYPE=Debug -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON
-    -DCMAKE_TOOLCHAIN_FILE=Resources/MinGWToolchain.cmake
+    -DCMAKE_TOOLCHAIN_FILE=Resources/MinGWToolchain.cmake -DUSE_DCMTK_360=ON -DUSE_LEGACY_JSONCPP=ON
     ..; fi
   - if [ $TRAVIS_OS_NAME == osx ]; then cmake 
     -DCMAKE_BUILD_TYPE=Debug -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON
--- a/AUTHORS	Thu Oct 29 11:25:45 2015 +0100
+++ b/AUTHORS	Fri Oct 12 15:18:10 2018 +0200
@@ -6,28 +6,15 @@
 ------------------
 
 * Sebastien Jodogne <s.jodogne@gmail.com>
-  Department of Medical Physics
-  University Hospital of Liege
-  Belgium
 
   Overall design and lead developer.
 
-
-Client library
---------------
-
-The client library of Orthanc is automatically wrapped from the C++
-code using the LAAW software. LAAW is the Lightweight, Automated API
-Wrapper from the Jomago team, that comes from the following authors:
+* Department of Medical Physics
+  University Hospital of Liege
+  4000 Liege
+  Belgium
 
-* Sebastien Jodogne <s.jodogne@gmail.com>
-* Alain Mazy <alain@mazy.be>
-* Benjamin Golinvaux <golinvauxb@gmail.com>
-
-LAAW should be soon released as a separate open-source project.
-
-
-Contributors
-------------
-
-See the file "THANKS" for the occasional contributors.
+* Osimis S.A. <info@osimis.io>
+  Rue du Bois Saint-Jean 15/1
+  4102 Seraing
+  Belgium
--- a/CMakeLists.txt	Thu Oct 29 11:25:45 2015 +0100
+++ b/CMakeLists.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -1,575 +1,525 @@
-cmake_minimum_required(VERSION 2.8)
-
-project(Orthanc)
-
-# Version of the build, should always be "mainline" except in release branches
-set(ORTHANC_VERSION "mainline")
-
-# Version of the database schema. History:
-#   * Orthanc 0.1.0 -> Orthanc 0.3.0 = no versioning
-#   * Orthanc 0.3.1                  = version 2
-#   * Orthanc 0.4.0 -> Orthanc 0.7.2 = version 3
-#   * Orthanc 0.7.3 -> Orthanc 0.8.4 = version 4
-#   * Orthanc 0.8.5 -> Orthanc 0.9.4 = version 5
-#   * Orthanc 0.9.5 -> mainline      = version 6
-set(ORTHANC_DATABASE_VERSION 6)
-
-
-#####################################################################
-## CMake parameters tunable at the command line
-#####################################################################
-
-# Parameters of the build
-SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
-SET(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)")
-SET(ENABLE_SSL ON CACHE BOOL "Include support for SSL")
-SET(DCMTK_DICTIONARY_DIR "" CACHE PATH "Directory containing the DCMTK dictionaries \"dicom.dic\" and \"private.dic\" (only when using system version of DCMTK)") 
-SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages")
-SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests")
-SET(ENABLE_GOOGLE_LOG OFF CACHE BOOL "Enable Google Log (otherwise, an internal logger is used)")
-SET(ENABLE_JPEG ON CACHE BOOL "Enable JPEG decompression")
-SET(ENABLE_JPEG_LOSSLESS ON CACHE BOOL "Enable JPEG-LS (Lossless) decompression")
-SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins")
-SET(BUILD_SERVE_FOLDERS ON CACHE BOOL "Build the ServeFolders plugin")
-
-# Advanced parameters to fine-tune linking against system libraries
-SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp")
-SET(USE_SYSTEM_GOOGLE_LOG ON CACHE BOOL "Use the system version of Google Log")
-SET(USE_SYSTEM_GOOGLE_TEST ON CACHE BOOL "Use the system version of Google Test")
-SET(USE_SYSTEM_SQLITE ON CACHE BOOL "Use the system version of SQLite")
-SET(USE_SYSTEM_MONGOOSE ON CACHE BOOL "Use the system version of Mongoose")
-SET(USE_SYSTEM_LUA ON CACHE BOOL "Use the system version of Lua")
-SET(USE_SYSTEM_DCMTK ON CACHE BOOL "Use the system version of DCMTK")
-SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost")
-SET(USE_SYSTEM_LIBPNG ON CACHE BOOL "Use the system version of libpng")
-SET(USE_SYSTEM_LIBJPEG ON CACHE BOOL "Use the system version of libjpeg")
-SET(USE_SYSTEM_CURL ON CACHE BOOL "Use the system version of LibCurl")
-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(USE_SYSTEM_PUGIXML ON CACHE BOOL "Use the system version of Pugixml)")
-
-# Experimental options
-SET(USE_PUGIXML ON CACHE BOOL "Use the Pugixml parser (turn off only for debug)")
-
-# Distribution-specific settings
-SET(USE_GTEST_DEBIAN_SOURCE_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)")
-SET(SYSTEM_MONGOOSE_USE_CALLBACKS ON CACHE BOOL "The system version of Mongoose uses callbacks (version >= 3.7)")
-SET(USE_BOOST_ICONV ON CACHE BOOL "Use iconv instead of wconv (Windows only)")
-
-mark_as_advanced(USE_GTEST_DEBIAN_SOURCE_PACKAGE)
-mark_as_advanced(SYSTEM_MONGOOSE_USE_CALLBACKS)
-mark_as_advanced(USE_BOOST_ICONV)
-mark_as_advanced(USE_PUGIXML)
-
-# Path to the root folder of the Orthanc distribution
-set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR})
-
-# Some basic inclusions
-include(CheckIncludeFiles)
-include(CheckIncludeFileCXX)
-include(CheckLibraryExists)
-include(FindPythonInterp)
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/AutoGeneratedCode.cmake)
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/DownloadPackage.cmake)
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/Compiler.cmake)
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/VisualStudioPrecompiledHeaders.cmake)
-
-
-
-
-#####################################################################
-## List of source files
-#####################################################################
-
-set(ORTHANC_CORE_SOURCES
-  Core/Cache/MemoryCache.cpp
-  Core/Cache/SharedArchive.cpp
-  Core/ChunkedBuffer.cpp
-  Core/Compression/DeflateBaseCompressor.cpp
-  Core/Compression/GzipCompressor.cpp
-  Core/Compression/HierarchicalZipWriter.cpp
-  Core/Compression/ZipWriter.cpp
-  Core/Compression/ZlibCompressor.cpp
-  Core/DicomFormat/DicomArray.cpp
-  Core/DicomFormat/DicomMap.cpp
-  Core/DicomFormat/DicomTag.cpp
-  Core/DicomFormat/DicomImageInformation.cpp
-  Core/DicomFormat/DicomIntegerPixelAccessor.cpp
-  Core/DicomFormat/DicomInstanceHasher.cpp
-  Core/DicomFormat/DicomValue.cpp
-  Core/Enumerations.cpp
-  Core/FileStorage/FilesystemStorage.cpp
-  Core/FileStorage/StorageAccessor.cpp
-  Core/HttpClient.cpp
-  Core/HttpServer/BufferHttpSender.cpp
-  Core/HttpServer/EmbeddedResourceHttpHandler.cpp
-  Core/HttpServer/FilesystemHttpHandler.cpp
-  Core/HttpServer/HttpToolbox.cpp
-  Core/HttpServer/HttpOutput.cpp
-  Core/HttpServer/StringHttpOutput.cpp
-  Core/HttpServer/MongooseServer.cpp
-  Core/HttpServer/HttpFileSender.cpp
-  Core/HttpServer/FilesystemHttpSender.cpp
-  Core/HttpServer/HttpStreamTranscoder.cpp
-  Core/Logging.cpp
-  Core/RestApi/RestApiCall.cpp
-  Core/RestApi/RestApiGetCall.cpp
-  Core/RestApi/RestApiHierarchy.cpp
-  Core/RestApi/RestApiPath.cpp
-  Core/RestApi/RestApiOutput.cpp
-  Core/RestApi/RestApi.cpp
-  Core/MultiThreading/Mutex.cpp
-  Core/MultiThreading/ReaderWriterLock.cpp
-  Core/MultiThreading/RunnableWorkersPool.cpp
-  Core/MultiThreading/Semaphore.cpp
-  Core/MultiThreading/SharedMessageQueue.cpp
-  Core/Images/Font.cpp
-  Core/Images/FontRegistry.cpp
-  Core/Images/ImageAccessor.cpp
-  Core/Images/ImageBuffer.cpp
-  Core/Images/ImageProcessing.cpp
-  Core/Images/JpegErrorManager.cpp
-  Core/Images/JpegReader.cpp
-  Core/Images/JpegWriter.cpp
-  Core/Images/PngReader.cpp
-  Core/Images/PngWriter.cpp
-  Core/SQLite/Connection.cpp
-  Core/SQLite/FunctionContext.cpp
-  Core/SQLite/Statement.cpp
-  Core/SQLite/StatementId.cpp
-  Core/SQLite/StatementReference.cpp
-  Core/SQLite/Transaction.cpp
-  Core/Toolbox.cpp
-  Core/Uuid.cpp
-  Core/Lua/LuaContext.cpp
-  Core/Lua/LuaFunctionCall.cpp
-  )
-
-
-set(ORTHANC_SERVER_SOURCES
-  OrthancServer/DatabaseWrapper.cpp
-  OrthancServer/DatabaseWrapperBase.cpp
-  OrthancServer/DicomDirWriter.cpp
-  OrthancServer/DicomModification.cpp
-  OrthancServer/DicomProtocol/DicomFindAnswers.cpp
-  OrthancServer/DicomProtocol/DicomServer.cpp
-  OrthancServer/DicomProtocol/DicomUserConnection.cpp
-  OrthancServer/DicomProtocol/RemoteModalityParameters.cpp
-  OrthancServer/DicomProtocol/ReusableDicomUserConnection.cpp
-  OrthancServer/ExportedResource.cpp
-  OrthancServer/FromDcmtkBridge.cpp
-  OrthancServer/Internals/CommandDispatcher.cpp
-  OrthancServer/Internals/DicomImageDecoder.cpp
-  OrthancServer/Internals/FindScp.cpp
-  OrthancServer/Internals/MoveScp.cpp
-  OrthancServer/Internals/StoreScp.cpp
-  OrthancServer/LuaScripting.cpp
-  OrthancServer/OrthancFindRequestHandler.cpp
-  OrthancServer/OrthancHttpHandler.cpp
-  OrthancServer/OrthancInitialization.cpp
-  OrthancServer/OrthancMoveRequestHandler.cpp
-  OrthancServer/OrthancPeerParameters.cpp
-  OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp
-  OrthancServer/OrthancRestApi/OrthancRestApi.cpp
-  OrthancServer/OrthancRestApi/OrthancRestArchive.cpp
-  OrthancServer/OrthancRestApi/OrthancRestChanges.cpp
-  OrthancServer/OrthancRestApi/OrthancRestModalities.cpp
-  OrthancServer/OrthancRestApi/OrthancRestResources.cpp
-  OrthancServer/OrthancRestApi/OrthancRestSystem.cpp
-  OrthancServer/ParsedDicomFile.cpp
-  OrthancServer/QueryRetrieveHandler.cpp
-  OrthancServer/Search/LookupIdentifierQuery.cpp
-  OrthancServer/Search/LookupResource.cpp
-  OrthancServer/Search/SetOfResources.cpp
-  OrthancServer/Search/ListConstraint.cpp
-  OrthancServer/Search/RangeConstraint.cpp
-  OrthancServer/Search/ValueConstraint.cpp
-  OrthancServer/Search/WildcardConstraint.cpp
-  OrthancServer/ServerContext.cpp
-  OrthancServer/ServerEnumerations.cpp
-  OrthancServer/ServerIndex.cpp
-  OrthancServer/ServerToolbox.cpp
-  OrthancServer/SliceOrdering.cpp
-  OrthancServer/ToDcmtkBridge.cpp
-
-  # From "lua-scripting" branch
-  OrthancServer/DicomInstanceToStore.cpp
-  OrthancServer/Scheduler/DeleteInstanceCommand.cpp
-  OrthancServer/Scheduler/ModifyInstanceCommand.cpp
-  OrthancServer/Scheduler/ServerCommandInstance.cpp
-  OrthancServer/Scheduler/ServerJob.cpp
-  OrthancServer/Scheduler/ServerScheduler.cpp
-  OrthancServer/Scheduler/StorePeerCommand.cpp
-  OrthancServer/Scheduler/StoreScuCommand.cpp
-  OrthancServer/Scheduler/CallSystemCommand.cpp
-  )
-
-
-set(ORTHANC_UNIT_TESTS_SOURCES
-  UnitTestsSources/DicomMapTests.cpp
-  UnitTestsSources/FileStorageTests.cpp
-  UnitTestsSources/FromDcmtkTests.cpp
-  UnitTestsSources/MemoryCacheTests.cpp
-  UnitTestsSources/ImageTests.cpp
-  UnitTestsSources/RestApiTests.cpp
-  UnitTestsSources/SQLiteTests.cpp
-  UnitTestsSources/SQLiteChromiumTests.cpp
-  UnitTestsSources/ServerIndexTests.cpp
-  UnitTestsSources/VersionsTests.cpp
-  UnitTestsSources/ZipTests.cpp
-  UnitTestsSources/LuaTests.cpp
-  UnitTestsSources/MultiThreadingTests.cpp
-  UnitTestsSources/UnitTestsMain.cpp
-  UnitTestsSources/ImageProcessingTests.cpp
-  UnitTestsSources/JpegLosslessTests.cpp
-  UnitTestsSources/StreamTests.cpp
-  )
-
-
-if (ENABLE_PLUGINS)
-  list(APPEND ORTHANC_SERVER_SOURCES
-    Plugins/Engine/OrthancPluginDatabase.cpp
-    Plugins/Engine/OrthancPlugins.cpp
-    Plugins/Engine/PluginsEnumerations.cpp
-    Plugins/Engine/PluginsErrorDictionary.cpp
-    Plugins/Engine/PluginsManager.cpp
-    Plugins/Engine/SharedLibrary.cpp
-    )
-
-  list(APPEND ORTHANC_UNIT_TESTS_SOURCES
-    UnitTestsSources/PluginsTests.cpp
-    )
-endif()
-
-
-set(ORTHANC_EMBEDDED_FILES
-  PREPARE_DATABASE            ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql
-  UPGRADE_DATABASE_3_TO_4     ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade3To4.sql
-  UPGRADE_DATABASE_4_TO_5     ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade4To5.sql
-  CONFIGURATION_SAMPLE        ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json
-  DICOM_CONFORMANCE_STATEMENT ${CMAKE_CURRENT_SOURCE_DIR}/Resources/DicomConformanceStatement.txt
-  LUA_TOOLBOX                 ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua
-  FONT_UBUNTU_MONO_BOLD_16    ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json
-  )
-
-
-
-#####################################################################
-## Inclusion of third-party dependencies
-#####################################################################
-
-if (ENABLE_GOOGLE_LOG)
-  include(${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleLogConfiguration.cmake)
-endif()
-
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/JsonCppConfiguration.cmake)
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/LibCurlConfiguration.cmake)
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/LibPngConfiguration.cmake)
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/LibJpegConfiguration.cmake)
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/LuaConfiguration.cmake)
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/MongooseConfiguration.cmake)
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/PugixmlConfiguration.cmake)
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/SQLiteConfiguration.cmake)
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/ZlibConfiguration.cmake)
-
-# These are the two most heavyweight dependencies. We put them as the
-# last includes to quickly spot problems when configuring static
-# builds.
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/BoostConfiguration.cmake)
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/DcmtkConfiguration.cmake)
-
-
-if (ENABLE_SSL)
-  add_definitions(-DORTHANC_SSL_ENABLED=1)
-  include(${CMAKE_SOURCE_DIR}/Resources/CMake/OpenSslConfiguration.cmake)
-else()
-  add_definitions(-DORTHANC_SSL_ENABLED=0)
-endif()
-
-
-if (ENABLE_JPEG)
-  add_definitions(-DORTHANC_JPEG_ENABLED=1)
-else()
-  add_definitions(-DORTHANC_JPEG_ENABLED=0)
-endif()
-
-
-if (ENABLE_JPEG_LOSSLESS)
-  add_definitions(-DORTHANC_JPEG_LOSSLESS_ENABLED=1)
-else()
-  add_definitions(-DORTHANC_JPEG_LOSSLESS_ENABLED=0)
-endif()
-
-
-if (ENABLE_PLUGINS)
-  add_definitions(-DORTHANC_PLUGINS_ENABLED=1)
-else()
-  add_definitions(-DORTHANC_PLUGINS_ENABLED=0)
-endif()
-
-
-
-#####################################################################
-## Autogeneration of files
-#####################################################################
-
-if (STANDALONE_BUILD)
-  # We embed all the resources in the binaries for standalone builds
-  add_definitions(-DORTHANC_STANDALONE=1)
-  EmbedResources(
-    ${ORTHANC_EMBEDDED_FILES}
-    ORTHANC_EXPLORER ${CMAKE_CURRENT_SOURCE_DIR}/OrthancExplorer
-    ${DCMTK_DICTIONARIES}
-    )
-else()
-  add_definitions(
-    -DORTHANC_STANDALONE=0
-    -DORTHANC_PATH=\"${CMAKE_SOURCE_DIR}\"
-    )
-  EmbedResources(
-    ${ORTHANC_EMBEDDED_FILES}
-    )
-endif()
-
-if(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-  execute_process(
-    COMMAND 
-    ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py
-    ${ORTHANC_VERSION} Orthanc Orthanc.exe "Lightweight, RESTful DICOM server for medical imaging"
-    ERROR_VARIABLE Failure
-    OUTPUT_FILE ${AUTOGENERATED_DIR}/Orthanc.rc
-    )
-
-  if (Failure)
-    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
-  endif()
-
-  list(APPEND ORTHANC_RESOURCES ${AUTOGENERATED_DIR}/Orthanc.rc)
-endif()
-
-
-
-#####################################################################
-## Build the core of Orthanc
-#####################################################################
-
-# Setup precompiled headers for Microsoft Visual Studio
-if (MSVC)
-  add_definitions(-DORTHANC_USE_PRECOMPILED_HEADERS=1)
-
-  ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS(
-    "PrecompiledHeaders.h" "Core/PrecompiledHeaders.cpp" ORTHANC_CORE_SOURCES)
-
-  ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS(
-    "PrecompiledHeadersServer.h" "OrthancServer/PrecompiledHeadersServer.cpp" ORTHANC_SERVER_SOURCES)
-
-  ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS(
-    "PrecompiledHeadersUnitTests.h" "UnitTestsSources/PrecompiledHeadersUnitTests.cpp" ORTHANC_UNIT_TESTS_SOURCES)
-endif()
-
-
-add_definitions(
-  -DORTHANC_VERSION="${ORTHANC_VERSION}"
-  -DORTHANC_DATABASE_VERSION=${ORTHANC_DATABASE_VERSION}
-  -DORTHANC_ENABLE_LOGGING=1
-  )
-
-list(LENGTH OPENSSL_SOURCES OPENSSL_SOURCES_LENGTH)
-if (${OPENSSL_SOURCES_LENGTH} GREATER 0)
-  add_library(OpenSSL STATIC ${OPENSSL_SOURCES})
-endif()
-
-add_library(CoreLibrary
-  STATIC
-  ${ORTHANC_CORE_SOURCES}
-  ${AUTOGENERATED_SOURCES}
-
-  ${BOOST_SOURCES}
-  ${CURL_SOURCES}
-  ${GOOGLE_LOG_SOURCES}
-  ${JSONCPP_SOURCES}
-  ${LIBPNG_SOURCES}
-  ${LIBJPEG_SOURCES}
-  ${LUA_SOURCES}
-  ${MONGOOSE_SOURCES}
-  ${PUGIXML_SOURCES}
-  ${SQLITE_SOURCES}
-  ${ZLIB_SOURCES}
-
-  ${CMAKE_SOURCE_DIR}/Resources/ThirdParty/md5/md5.c
-  ${CMAKE_SOURCE_DIR}/Resources/ThirdParty/base64/base64.cpp
-
-  # This is the minizip distribution to create ZIP files using zlib
-  ${ORTHANC_ROOT}/Resources/ThirdParty/minizip/ioapi.c
-  ${ORTHANC_ROOT}/Resources/ThirdParty/minizip/zip.c
-  )  
-
-
-
-#####################################################################
-## Build the Orthanc server
-#####################################################################
-
-add_library(ServerLibrary
-  STATIC
-  ${DCMTK_SOURCES}
-  ${ORTHANC_SERVER_SOURCES}
-  )
-
-# Ensure autogenerated code is built before building ServerLibrary
-add_dependencies(ServerLibrary CoreLibrary)
-
-add_executable(Orthanc
-  OrthancServer/main.cpp
-  ${ORTHANC_RESOURCES}
-  )
-
-target_link_libraries(Orthanc ServerLibrary CoreLibrary ${DCMTK_LIBRARIES})
-
-if (${OPENSSL_SOURCES_LENGTH} GREATER 0)
-  target_link_libraries(Orthanc OpenSSL)
-endif()
-
-install(
-  TARGETS Orthanc
-  RUNTIME DESTINATION sbin
-  )
-
-
-
-#####################################################################
-## Build the unit tests
-#####################################################################
-
-if (UNIT_TESTS_WITH_HTTP_CONNEXIONS)
-  add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=1)
-else()
-  add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=0)
-endif()
-
-add_definitions(
-  -DORTHANC_BUILD_UNIT_TESTS=1
-  )
-
-include(${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleTestConfiguration.cmake)
-add_executable(UnitTests
-  ${GTEST_SOURCES}
-  ${ORTHANC_UNIT_TESTS_SOURCES}
-  )
-target_link_libraries(UnitTests ServerLibrary CoreLibrary ${DCMTK_LIBRARIES})
-
-if (${OPENSSL_SOURCES_LENGTH} GREATER 0)
-  target_link_libraries(UnitTests OpenSSL)
-endif()
-
-
-
-#####################################################################
-## Build the "ServeFolders" plugin
-#####################################################################
-
-if (ENABLE_PLUGINS AND BUILD_SERVE_FOLDERS)
-  execute_process(
-    COMMAND 
-    ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py
-    ${ORTHANC_VERSION} ServeFolders ServeFolders.dll "Orthanc plugin to serve additional folders"
-    ERROR_VARIABLE Failure
-    OUTPUT_FILE ${AUTOGENERATED_DIR}/ServeFolders.rc
-    )
-
-  if (Failure)
-    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
-  endif()
-
-  add_definitions(-DSERVE_FOLDERS_VERSION="${ORTHANC_VERSION}")
-
-  include_directories(${CMAKE_SOURCE_DIR}/Plugins/Include)
-
-  add_library(ServeFolders SHARED 
-    ${BOOST_SOURCES}
-    ${JSONCPP_SOURCES}
-    Plugins/Samples/ServeFolders/Plugin.cpp
-    ${AUTOGENERATED_DIR}/ServeFolders.rc
-    )
-
-  set_target_properties(
-    ServeFolders PROPERTIES 
-    VERSION ${ORTHANC_VERSION} 
-    SOVERSION ${ORTHANC_VERSION}
-    )
-
-  install(
-    TARGETS ServeFolders
-    RUNTIME DESTINATION lib    # Destination for Windows
-    LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
-    )
-endif()
-
-
-
-#####################################################################
-## Generate the documentation if Doxygen is present
-#####################################################################
-
-find_package(Doxygen)
-if (DOXYGEN_FOUND)
-  configure_file(
-    ${CMAKE_SOURCE_DIR}/Resources/Orthanc.doxygen
-    ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen
-    @ONLY)
-
-  configure_file(
-    ${CMAKE_SOURCE_DIR}/Resources/OrthancPlugin.doxygen
-    ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen
-    @ONLY)
-
-  add_custom_target(doc
-    ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen
-    COMMENT "Generating internal documentation with Doxygen" VERBATIM
-    )
-
-  add_custom_command(TARGET Orthanc
-    POST_BUILD
-    COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen
-    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
-    COMMENT "Generating plugin documentation with Doxygen" VERBATIM
-    )
-
-  install(
-    DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/OrthancPluginDocumentation/doc/
-    DESTINATION share/doc/orthanc/OrthancPlugin
-    )
-else()
-  message("Doxygen not found. The documentation will not be built.")
-endif()
-
-
-
-#####################################################################
-## Install the plugin SDK
-#####################################################################
-
-if (ENABLE_PLUGINS)
-  install(
-    FILES
-    Plugins/Include/orthanc/OrthancCPlugin.h 
-    Plugins/Include/orthanc/OrthancCDatabasePlugin.h 
-    Plugins/Include/orthanc/OrthancCppDatabasePlugin.h 
-    DESTINATION include/orthanc
-    )
-endif()
-
-
-
-#####################################################################
-## Prepare the "uninstall" target
-## http://www.cmake.org/Wiki/CMake_FAQ#Can_I_do_.22make_uninstall.22_with_CMake.3F
-#####################################################################
-
-configure_file(
-    "${CMAKE_CURRENT_SOURCE_DIR}/Resources/CMake/Uninstall.cmake.in"
-    "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake"
-    IMMEDIATE @ONLY)
-
-add_custom_target(uninstall
-    COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake)
+cmake_minimum_required(VERSION 2.8)
+
+project(Orthanc)
+
+
+#####################################################################
+## Generic parameters of the Orthanc framework
+#####################################################################
+
+include(${CMAKE_SOURCE_DIR}/Resources/CMake/OrthancFrameworkParameters.cmake)
+
+# Enable all the optional components of the Orthanc framework
+set(ENABLE_CRYPTO_OPTIONS ON)
+set(ENABLE_DCMTK ON)
+set(ENABLE_DCMTK_NETWORKING ON)
+set(ENABLE_GOOGLE_TEST ON)
+set(ENABLE_JPEG ON)
+set(ENABLE_LOCALE ON)
+set(ENABLE_LUA ON)
+set(ENABLE_PNG ON)
+set(ENABLE_PUGIXML ON)
+set(ENABLE_SQLITE ON)
+set(ENABLE_WEB_CLIENT ON)
+set(ENABLE_WEB_SERVER ON)
+set(ENABLE_ZLIB ON)
+
+set(HAS_EMBEDDED_RESOURCES ON)
+
+
+#####################################################################
+## CMake parameters tunable at the command line to configure the
+## plugins, the companion tools, and the unit tests
+#####################################################################
+
+# Parameters of the build
+SET(BUILD_MODALITY_WORKLISTS ON CACHE BOOL "Whether to build the sample plugin to serve modality worklists")
+SET(BUILD_RECOVER_COMPRESSED_FILE ON CACHE BOOL "Whether to build the companion tool to recover files compressed using Orthanc")
+SET(BUILD_SERVE_FOLDERS ON CACHE BOOL "Whether to build the ServeFolders plugin")
+SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins")
+SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests")
+
+
+#####################################################################
+## Configuration of the Orthanc framework
+#####################################################################
+
+include(${CMAKE_SOURCE_DIR}/Resources/CMake/VisualStudioPrecompiledHeaders.cmake)
+include(${CMAKE_SOURCE_DIR}/Resources/CMake/OrthancFrameworkConfiguration.cmake)
+
+
+#####################################################################
+## List of source files
+#####################################################################
+
+set(ORTHANC_SERVER_SOURCES
+  OrthancServer/DatabaseWrapper.cpp
+  OrthancServer/DicomInstanceOrigin.cpp
+  OrthancServer/DicomInstanceToStore.cpp
+  OrthancServer/ExportedResource.cpp
+  OrthancServer/LuaScripting.cpp
+  OrthancServer/OrthancFindRequestHandler.cpp
+  OrthancServer/OrthancHttpHandler.cpp
+  OrthancServer/OrthancInitialization.cpp
+  OrthancServer/OrthancMoveRequestHandler.cpp
+  OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp
+  OrthancServer/OrthancRestApi/OrthancRestApi.cpp
+  OrthancServer/OrthancRestApi/OrthancRestArchive.cpp
+  OrthancServer/OrthancRestApi/OrthancRestChanges.cpp
+  OrthancServer/OrthancRestApi/OrthancRestModalities.cpp
+  OrthancServer/OrthancRestApi/OrthancRestResources.cpp
+  OrthancServer/OrthancRestApi/OrthancRestSystem.cpp
+  OrthancServer/QueryRetrieveHandler.cpp
+  OrthancServer/Search/HierarchicalMatcher.cpp
+  OrthancServer/Search/IFindConstraint.cpp
+  OrthancServer/Search/ListConstraint.cpp
+  OrthancServer/Search/LookupIdentifierQuery.cpp
+  OrthancServer/Search/LookupResource.cpp
+  OrthancServer/Search/RangeConstraint.cpp
+  OrthancServer/Search/SetOfResources.cpp
+  OrthancServer/Search/ValueConstraint.cpp
+  OrthancServer/Search/WildcardConstraint.cpp
+  OrthancServer/ServerContext.cpp
+  OrthancServer/ServerEnumerations.cpp
+  OrthancServer/ServerIndex.cpp
+  OrthancServer/ServerJobs/ArchiveJob.cpp
+  OrthancServer/ServerJobs/DicomModalityStoreJob.cpp
+  OrthancServer/ServerJobs/DicomMoveScuJob.cpp
+  OrthancServer/ServerJobs/LuaJobManager.cpp
+  OrthancServer/ServerJobs/MergeStudyJob.cpp
+  OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp
+  OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp
+  OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp
+  OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp
+  OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp
+  OrthancServer/ServerJobs/OrthancJobUnserializer.cpp
+  OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp
+  OrthancServer/ServerJobs/ResourceModificationJob.cpp
+  OrthancServer/ServerJobs/SplitStudyJob.cpp
+  OrthancServer/ServerToolbox.cpp
+  OrthancServer/SliceOrdering.cpp
+  )
+
+
+set(ORTHANC_UNIT_TESTS_SOURCES
+  UnitTestsSources/DicomMapTests.cpp
+  UnitTestsSources/FileStorageTests.cpp
+  UnitTestsSources/FromDcmtkTests.cpp
+  UnitTestsSources/MemoryCacheTests.cpp
+  UnitTestsSources/ImageTests.cpp
+  UnitTestsSources/RestApiTests.cpp
+  UnitTestsSources/SQLiteTests.cpp
+  UnitTestsSources/SQLiteChromiumTests.cpp
+  UnitTestsSources/ServerIndexTests.cpp
+  UnitTestsSources/VersionsTests.cpp
+  UnitTestsSources/ZipTests.cpp
+  UnitTestsSources/LuaTests.cpp
+  UnitTestsSources/MultiThreadingTests.cpp
+  UnitTestsSources/UnitTestsMain.cpp
+  UnitTestsSources/ImageProcessingTests.cpp
+  UnitTestsSources/JpegLosslessTests.cpp
+  UnitTestsSources/StreamTests.cpp
+  )
+
+
+if (ENABLE_PLUGINS)
+  list(APPEND ORTHANC_SERVER_SOURCES
+    Plugins/Engine/OrthancPluginDatabase.cpp
+    Plugins/Engine/OrthancPlugins.cpp
+    Plugins/Engine/PluginsEnumerations.cpp
+    Plugins/Engine/PluginsErrorDictionary.cpp
+    Plugins/Engine/PluginsJob.cpp
+    Plugins/Engine/PluginsManager.cpp
+    )
+
+  list(APPEND ORTHANC_UNIT_TESTS_SOURCES
+    UnitTestsSources/PluginsTests.cpp
+    )
+endif()
+
+
+if (CMAKE_COMPILER_IS_GNUCXX
+    AND NOT CMAKE_CROSSCOMPILING 
+    AND USE_DCMTK_360)
+  # Add the "-pedantic" flag only on the Orthanc sources, and only if
+  # cross-compiling DCMTK 3.6.0
+  set(ORTHANC_ALL_SOURCES
+    ${ORTHANC_CORE_SOURCES_INTERNAL}
+    ${ORTHANC_DICOM_SOURCES_INTERNAL}
+    ${ORTHANC_SERVER_SOURCES}
+    ${ORTHANC_UNIT_TESTS_SOURCES}
+    Plugins/Samples/ServeFolders/Plugin.cpp
+    Plugins/Samples/ModalityWorklists/Plugin.cpp
+    OrthancServer/main.cpp
+    )
+
+  set_source_files_properties(${ORTHANC_ALL_SOURCES}
+    PROPERTIES COMPILE_FLAGS -pedantic
+    )
+endif()
+
+
+#####################################################################
+## Autogeneration of files
+#####################################################################
+
+set(ORTHANC_EMBEDDED_FILES
+  PREPARE_DATABASE            ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql
+  UPGRADE_DATABASE_3_TO_4     ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade3To4.sql
+  UPGRADE_DATABASE_4_TO_5     ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade4To5.sql
+  CONFIGURATION_SAMPLE        ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json
+  DICOM_CONFORMANCE_STATEMENT ${CMAKE_CURRENT_SOURCE_DIR}/Resources/DicomConformanceStatement.txt
+  LUA_TOOLBOX                 ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua
+  FONT_UBUNTU_MONO_BOLD_16    ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json
+  )
+
+if (STANDALONE_BUILD)
+  # We embed all the resources in the binaries for standalone builds
+  add_definitions(-DORTHANC_STANDALONE=1)
+  EmbedResources(
+    ${ORTHANC_EMBEDDED_FILES}
+    ORTHANC_EXPLORER ${CMAKE_CURRENT_SOURCE_DIR}/OrthancExplorer
+    ${DCMTK_DICTIONARIES}
+    )
+else()
+  add_definitions(
+    -DORTHANC_STANDALONE=0
+    -DORTHANC_PATH=\"${CMAKE_SOURCE_DIR}\"
+    )
+  EmbedResources(
+    ${ORTHANC_EMBEDDED_FILES}
+    )
+endif()
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  execute_process(
+    COMMAND 
+    ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py
+    ${ORTHANC_VERSION} Orthanc Orthanc.exe "Lightweight, RESTful DICOM server for medical imaging"
+    ERROR_VARIABLE Failure
+    OUTPUT_FILE ${AUTOGENERATED_DIR}/Orthanc.rc
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+  endif()
+
+  list(APPEND ORTHANC_RESOURCES ${AUTOGENERATED_DIR}/Orthanc.rc)
+endif()
+
+
+
+#####################################################################
+## Configuration of the C/C++ macros
+#####################################################################
+
+if (ENABLE_PLUGINS)
+  add_definitions(-DORTHANC_ENABLE_PLUGINS=1)
+else()
+  add_definitions(-DORTHANC_ENABLE_PLUGINS=0)
+endif()
+
+
+if (UNIT_TESTS_WITH_HTTP_CONNEXIONS)
+  add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=1)
+else()
+  add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=0)
+endif()
+
+
+include_directories(${CMAKE_SOURCE_DIR}/Plugins/Include)
+
+add_definitions(
+  -DORTHANC_BUILD_UNIT_TESTS=1
+  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
+  
+  # Macros for the plugins
+  -DHAS_ORTHANC_EXCEPTION=0
+  -DMODALITY_WORKLISTS_VERSION="${ORTHANC_VERSION}"
+  -DSERVE_FOLDERS_VERSION="${ORTHANC_VERSION}"
+  )
+
+
+# Setup precompiled headers for Microsoft Visual Studio
+
+# WARNING: There must be NO MORE "add_definitions()", "include()" or
+# "include_directories()" below, otherwise the generated precompiled
+# headers might get broken!
+
+if (MSVC)
+  add_definitions(-DORTHANC_USE_PRECOMPILED_HEADERS=1)
+
+  set(TMP
+    ${ORTHANC_CORE_SOURCES_INTERNAL}
+    ${ORTHANC_DICOM_SOURCES_INTERNAL}
+    )
+  
+  ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS(
+    "PrecompiledHeaders.h" "Core/PrecompiledHeaders.cpp"
+    TMP ORTHANC_CORE_PCH)
+
+  ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS(
+    "PrecompiledHeadersServer.h" "OrthancServer/PrecompiledHeadersServer.cpp"
+    ORTHANC_SERVER_SOURCES ORTHANC_SERVER_PCH)
+
+  ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS(
+    "PrecompiledHeadersUnitTests.h" "UnitTestsSources/PrecompiledHeadersUnitTests.cpp"
+    ORTHANC_UNIT_TESTS_SOURCES ORTHANC_UNIT_TESTS_PCH)
+endif()
+
+
+
+#####################################################################
+## Build the core of Orthanc
+#####################################################################
+
+# "CoreLibrary" contains all the third-party dependencies and the
+# content of the "Core" folder
+add_library(CoreLibrary
+  STATIC
+  ${ORTHANC_CORE_PCH}
+  ${ORTHANC_CORE_SOURCES}
+  ${ORTHANC_DICOM_SOURCES}
+  ${AUTOGENERATED_SOURCES}
+  )  
+
+
+#####################################################################
+## Build the Orthanc server
+#####################################################################
+
+add_library(ServerLibrary
+  STATIC
+  ${ORTHANC_SERVER_PCH}
+  ${ORTHANC_SERVER_SOURCES}
+  )
+
+# Ensure autogenerated code is built before building ServerLibrary
+add_dependencies(ServerLibrary CoreLibrary)
+
+add_executable(Orthanc
+  OrthancServer/main.cpp
+  ${ORTHANC_RESOURCES}
+  )
+
+target_link_libraries(Orthanc ServerLibrary CoreLibrary ${DCMTK_LIBRARIES})
+
+install(
+  TARGETS Orthanc
+  RUNTIME DESTINATION sbin
+  )
+
+
+#####################################################################
+## Build the unit tests
+#####################################################################
+
+add_executable(UnitTests
+  ${GOOGLE_TEST_SOURCES}
+  ${ORTHANC_UNIT_TESTS_PCH}
+  ${ORTHANC_UNIT_TESTS_SOURCES}
+  )
+
+target_link_libraries(UnitTests
+  ServerLibrary
+  CoreLibrary
+  ${DCMTK_LIBRARIES}
+  ${GOOGLE_TEST_LIBRARIES}
+  )
+
+
+#####################################################################
+## Build the "ServeFolders" plugin
+#####################################################################
+
+if (ENABLE_PLUGINS AND BUILD_SERVE_FOLDERS)
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+    execute_process(
+      COMMAND 
+      ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py
+      ${ORTHANC_VERSION} ServeFolders ServeFolders.dll "Orthanc plugin to serve additional folders"
+      ERROR_VARIABLE Failure
+      OUTPUT_FILE ${AUTOGENERATED_DIR}/ServeFolders.rc
+      )
+
+    if (Failure)
+      message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+    endif()
+
+    list(APPEND SERVE_FOLDERS_RESOURCES ${AUTOGENERATED_DIR}/ServeFolders.rc)
+  endif()  
+
+  add_library(ServeFolders SHARED 
+    ${BOOST_SOURCES}
+    ${JSONCPP_SOURCES}
+    ${LIBICONV_SOURCES}
+    Plugins/Samples/ServeFolders/Plugin.cpp
+    Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
+    ${SERVE_FOLDERS_RESOURCES}
+    )
+
+  set_target_properties(
+    ServeFolders PROPERTIES 
+    VERSION ${ORTHANC_VERSION} 
+    SOVERSION ${ORTHANC_VERSION}
+    )
+
+  install(
+    TARGETS ServeFolders
+    RUNTIME DESTINATION lib    # Destination for Windows
+    LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+    )
+endif()
+
+
+
+#####################################################################
+## Build the "ModalityWorklists" plugin
+#####################################################################
+
+if (ENABLE_PLUGINS AND BUILD_MODALITY_WORKLISTS)
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+    execute_process(
+      COMMAND 
+      ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py
+      ${ORTHANC_VERSION} ModalityWorklists ModalityWorklists.dll "Sample Orthanc plugin to serve modality worklists"
+      ERROR_VARIABLE Failure
+      OUTPUT_FILE ${AUTOGENERATED_DIR}/ModalityWorklists.rc
+      )
+
+    if (Failure)
+      message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+    endif()
+
+    list(APPEND MODALITY_WORKLISTS_RESOURCES ${AUTOGENERATED_DIR}/ModalityWorklists.rc)
+  endif()
+
+  add_library(ModalityWorklists SHARED 
+    ${BOOST_SOURCES}
+    ${JSONCPP_SOURCES}
+    ${LIBICONV_SOURCES}
+    Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
+    Plugins/Samples/ModalityWorklists/Plugin.cpp
+    ${MODALITY_WORKLISTS_RESOURCES}
+    )
+
+  set_target_properties(
+    ModalityWorklists PROPERTIES 
+    VERSION ${ORTHANC_VERSION} 
+    SOVERSION ${ORTHANC_VERSION}
+    )
+
+  install(
+    TARGETS ModalityWorklists
+    RUNTIME DESTINATION lib    # Destination for Windows
+    LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+    )
+endif()
+
+
+
+#####################################################################
+## Build the companion tool to recover files compressed using Orthanc
+#####################################################################
+
+if (BUILD_RECOVER_COMPRESSED_FILE)
+  set(RECOVER_COMPRESSED_SOURCES
+    Resources/Samples/Tools/RecoverCompressedFile.cpp
+    )
+
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+    execute_process(
+      COMMAND 
+      ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py
+      ${ORTHANC_VERSION} OrthancRecoverCompressedFile OrthancRecoverCompressedFile.exe
+      "Lightweight, RESTful DICOM server for medical imaging"
+      ERROR_VARIABLE Failure
+      OUTPUT_FILE ${AUTOGENERATED_DIR}/OrthancRecoverCompressedFile.rc
+      )
+
+    if (Failure)
+      message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+    endif()
+
+    list(APPEND RECOVER_COMPRESSED_SOURCES
+      ${AUTOGENERATED_DIR}/OrthancRecoverCompressedFile.rc
+      )
+  endif()
+
+  add_executable(OrthancRecoverCompressedFile ${RECOVER_COMPRESSED_SOURCES})
+
+  target_link_libraries(OrthancRecoverCompressedFile CoreLibrary)
+
+  install(
+    TARGETS OrthancRecoverCompressedFile
+    RUNTIME DESTINATION bin
+    )
+endif()
+
+
+
+#####################################################################
+## Generate the documentation if Doxygen is present
+#####################################################################
+
+find_package(Doxygen)
+if (DOXYGEN_FOUND)
+  configure_file(
+    ${CMAKE_SOURCE_DIR}/Resources/Orthanc.doxygen
+    ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen
+    @ONLY)
+
+  configure_file(
+    ${CMAKE_SOURCE_DIR}/Resources/OrthancPlugin.doxygen
+    ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen
+    @ONLY)
+
+  add_custom_target(doc
+    ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen
+    COMMENT "Generating internal documentation with Doxygen" VERBATIM
+    )
+
+  add_custom_command(TARGET Orthanc
+    POST_BUILD
+    COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen
+    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+    COMMENT "Generating plugin documentation with Doxygen" VERBATIM
+    )
+
+  install(
+    DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/OrthancPluginDocumentation/doc/
+    DESTINATION share/doc/orthanc/OrthancPlugin
+    )
+else()
+  message("Doxygen not found. The documentation will not be built.")
+endif()
+
+
+
+#####################################################################
+## Install the plugin SDK
+#####################################################################
+
+if (ENABLE_PLUGINS)
+  install(
+    FILES
+    Plugins/Include/orthanc/OrthancCPlugin.h 
+    Plugins/Include/orthanc/OrthancCDatabasePlugin.h 
+    DESTINATION include/orthanc
+    )
+endif()
+
+
+
+#####################################################################
+## Prepare the "uninstall" target
+## http://www.cmake.org/Wiki/CMake_FAQ#Can_I_do_.22make_uninstall.22_with_CMake.3F
+#####################################################################
+
+configure_file(
+    "${CMAKE_CURRENT_SOURCE_DIR}/Resources/CMake/Uninstall.cmake.in"
+    "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake"
+    IMMEDIATE @ONLY)
+
+add_custom_target(uninstall
+    COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake)
--- a/Core/Cache/ICachePageProvider.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Cache/ICachePageProvider.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Cache/LeastRecentlyUsedIndex.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Cache/LeastRecentlyUsedIndex.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Cache/MemoryCache.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Cache/MemoryCache.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -77,6 +78,18 @@
   {
   }
 
+  void MemoryCache::Invalidate(const std::string& id)
+  {
+    Page* p = NULL;
+    if (index_.Contains(id, p))
+    {
+      VLOG(1) << "Invalidating a cache page";
+      assert(p != NULL);
+      delete p;
+      index_.Invalidate(id);
+    }
+  }
+
   MemoryCache::~MemoryCache()
   {
     while (!index_.IsEmpty())
--- a/Core/Cache/MemoryCache.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Cache/MemoryCache.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -63,5 +64,7 @@
     ~MemoryCache();
 
     IDynamicObject& Access(const std::string& id);
+
+    void Invalidate(const std::string& id);
   };
 }
--- a/Core/Cache/SharedArchive.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Cache/SharedArchive.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,8 +34,7 @@
 #include "../PrecompiledHeaders.h"
 #include "SharedArchive.h"
 
-
-#include "../Uuid.h"
+#include "../Toolbox.h"
 
 
 namespace Orthanc
--- a/Core/Cache/SharedArchive.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Cache/SharedArchive.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,6 +33,14 @@
 
 #pragma once
 
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The class SharedArchive cannot be used in sandboxed environments
+#endif
+
 #include "LeastRecentlyUsedIndex.h"
 #include "../IDynamicObject.h"
 
--- a/Core/ChunkedBuffer.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/ChunkedBuffer.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -51,17 +52,19 @@
   }
 
 
-  void ChunkedBuffer::AddChunk(const char* chunkData,
+  void ChunkedBuffer::AddChunk(const void* chunkData,
                                size_t chunkSize)
   {
     if (chunkSize == 0)
     {
       return;
     }
-
-    assert(chunkData != NULL);
-    chunks_.push_back(new std::string(chunkData, chunkSize));
-    numBytes_ += chunkSize;
+    else
+    {
+      assert(chunkData != NULL);
+      chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize));
+      numBytes_ += chunkSize;
+    }
   }
 
 
@@ -95,5 +98,6 @@
     }
 
     chunks_.clear();
+    numBytes_ = 0;
   }
 }
--- a/Core/ChunkedBuffer.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/ChunkedBuffer.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -61,7 +62,7 @@
       return numBytes_;
     }
 
-    void AddChunk(const char* chunkData,
+    void AddChunk(const void* chunkData,
                   size_t chunkSize);
 
     void AddChunk(const std::string& chunk);
--- a/Core/Compression/DeflateBaseCompressor.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Compression/DeflateBaseCompressor.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Compression/DeflateBaseCompressor.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Compression/DeflateBaseCompressor.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,6 +35,15 @@
 
 #include "IBufferCompressor.h"
 
+#if !defined(ORTHANC_ENABLE_ZLIB)
+#  error The macro ORTHANC_ENABLE_ZLIB must be defined
+#endif
+
+#if ORTHANC_ENABLE_ZLIB != 1
+#  error ZLIB support must be enabled to include this file
+#endif
+
+
 #include <stdint.h>
 
 namespace Orthanc
--- a/Core/Compression/GzipCompressor.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Compression/GzipCompressor.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Compression/GzipCompressor.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Compression/GzipCompressor.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Compression/HierarchicalZipWriter.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Compression/HierarchicalZipWriter.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Compression/HierarchicalZipWriter.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Compression/HierarchicalZipWriter.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -38,6 +39,10 @@
 #include <list>
 #include <boost/lexical_cast.hpp>
 
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+#  include <gtest/gtest_prod.h>
+#endif
+
 namespace Orthanc
 {
   class HierarchicalZipWriter
--- a/Core/Compression/IBufferCompressor.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Compression/IBufferCompressor.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Compression/ZipWriter.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Compression/ZipWriter.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Compression/ZipWriter.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Compression/ZipWriter.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,14 +33,19 @@
 
 #pragma once
 
+#if !defined(ORTHANC_ENABLE_ZLIB)
+#  error The macro ORTHANC_ENABLE_ZLIB must be defined
+#endif
+
+#if ORTHANC_ENABLE_ZLIB != 1
+#  error ZLIB support must be enabled to include this file
+#endif
+
+
 #include <stdint.h>
 #include <string>
 #include <boost/shared_ptr.hpp>
 
-#if ORTHANC_BUILD_UNIT_TESTS == 1
-#include <gtest/gtest_prod.h>
-#endif
-
 namespace Orthanc
 {
   class ZipWriter
--- a/Core/Compression/ZlibCompressor.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Compression/ZlibCompressor.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Compression/ZlibCompressor.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Compression/ZlibCompressor.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomArray.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomArray.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomArray.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomArray.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -47,7 +48,7 @@
     Elements  elements_;
 
   public:
-    DicomArray(const DicomMap& map);
+    explicit DicomArray(const DicomMap& map);
 
     ~DicomArray();
 
--- a/Core/DicomFormat/DicomElement.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomElement.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomImageInformation.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomImageInformation.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -44,6 +45,7 @@
 #include <limits>
 #include <cassert>
 #include <stdio.h>
+#include <memory>
 
 namespace Orthanc
 {
@@ -157,7 +159,7 @@
       if (samplesPerPixel_ > 1)
       {
         // The "Planar Configuration" is only set when "Samples per Pixels" is greater than 1
-        // https://www.dabsoft.ch/dicom/3/C.7.6.3.1.3/
+        // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.3
         try
         {
           planarConfiguration = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_PLANAR_CONFIGURATION).GetContent());
@@ -177,18 +179,20 @@
       throw OrthancException(ErrorCode_NotImplemented);
     }
 
-    try
-    {
-      numberOfFrames_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent());
-    }
-    catch (OrthancException&)
+    if (values.HasTag(DICOM_TAG_NUMBER_OF_FRAMES))
     {
-      // If the tag "NumberOfFrames" is absent, assume there is a single frame
-      numberOfFrames_ = 1;
+      try
+      {
+        numberOfFrames_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent());
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
     }
-    catch (boost::bad_lexical_cast&)
+    else
     {
-      throw OrthancException(ErrorCode_NotImplemented);
+      numberOfFrames_ = 1;
     }
 
     if ((bitsAllocated_ != 8 && bitsAllocated_ != 16 && 
@@ -199,13 +203,6 @@
       throw OrthancException(ErrorCode_NotImplemented);
     }
 
-    if (bitsAllocated_ > 32 ||
-        bitsStored_ >= 32)
-    {
-      // Not available, as the accessor internally uses int32_t values
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-
     if (samplesPerPixel_ == 0)
     {
       throw OrthancException(ErrorCode_NotImplemented);
@@ -217,10 +214,44 @@
     isSigned_ = (pixelRepresentation != 0 ? true : false);
   }
 
+  DicomImageInformation* DicomImageInformation::Clone() const
+  {
+    std::auto_ptr<DicomImageInformation> target(new DicomImageInformation);
+    target->width_ = width_;
+    target->height_ = height_;
+    target->samplesPerPixel_ = samplesPerPixel_;
+    target->numberOfFrames_ = numberOfFrames_;
+    target->isPlanar_ = isPlanar_;
+    target->isSigned_ = isSigned_;
+    target->bytesPerValue_ = bytesPerValue_;
+    target->bitsAllocated_ = bitsAllocated_;
+    target->bitsStored_ = bitsStored_;
+    target->highBit_ = highBit_;
+    target->photometric_ = photometric_;
 
-  bool DicomImageInformation::ExtractPixelFormat(PixelFormat& format) const
+    return target.release();
+  }
+
+  bool DicomImageInformation::ExtractPixelFormat(PixelFormat& format,
+                                                 bool ignorePhotometricInterpretation) const
   {
-    if (photometric_ == PhotometricInterpretation_Monochrome1 ||
+    if (photometric_ == PhotometricInterpretation_Palette)
+    {
+      if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned())
+      {
+        format = PixelFormat_RGB24;
+        return true;
+      }
+
+      if (GetBitsStored() == 16 && GetChannelCount() == 1 && !IsSigned())
+      {
+        format = PixelFormat_RGB48;
+        return true;
+      }
+    }
+    
+    if (ignorePhotometricInterpretation ||
+        photometric_ == PhotometricInterpretation_Monochrome1 ||
         photometric_ == PhotometricInterpretation_Monochrome2)
     {
       if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned())
@@ -240,12 +271,18 @@
         format = PixelFormat_SignedGrayscale16;
         return true;
       }
+      
+      if (GetBitsAllocated() == 32 && GetChannelCount() == 1 && !IsSigned())
+      {
+        format = PixelFormat_Grayscale32;
+        return true;
+      }
     }
 
     if (GetBitsStored() == 8 && 
         GetChannelCount() == 3 && 
         !IsSigned() &&
-        photometric_ == PhotometricInterpretation_RGB)
+        (ignorePhotometricInterpretation || photometric_ == PhotometricInterpretation_RGB))
     {
       format = PixelFormat_RGB24;
       return true;
@@ -253,4 +290,13 @@
 
     return false;
   }
+
+
+  size_t DicomImageInformation::GetFrameSize() const
+  {
+    return (GetHeight() * 
+            GetWidth() * 
+            GetBytesPerValue() * 
+            GetChannelCount());
+  }
 }
--- a/Core/DicomFormat/DicomImageInformation.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomImageInformation.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -56,8 +57,15 @@
 
     PhotometricInterpretation  photometric_;
 
+  protected:
+    explicit DicomImageInformation()
+    {
+    }
+
   public:
-    DicomImageInformation(const DicomMap& values);
+    explicit DicomImageInformation(const DicomMap& values);
+
+    DicomImageInformation* Clone() const;
 
     unsigned int GetWidth() const
     {
@@ -119,6 +127,9 @@
       return photometric_;
     }
 
-    bool ExtractPixelFormat(PixelFormat& format) const;
+    bool ExtractPixelFormat(PixelFormat& format,
+                            bool ignorePhotometricInterpretation) const;
+
+    size_t GetFrameSize() const;
   };
 }
--- a/Core/DicomFormat/DicomInstanceHasher.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomInstanceHasher.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomInstanceHasher.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomInstanceHasher.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomIntegerPixelAccessor.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomIntegerPixelAccessor.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -53,9 +54,15 @@
     pixelData_(pixelData),
     size_(size)
   {
+    if (information_.GetBitsAllocated() > 32 ||
+        information_.GetBitsStored() >= 32)
+    {
+      // Not available, as the accessor internally uses int32_t values
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
     frame_ = 0;
-    frameOffset_ = (information_.GetHeight() * information_.GetWidth() * 
-                    information_.GetBytesPerValue() * information_.GetChannelCount());
+    frameOffset_ = information_.GetFrameSize();
 
     if (information_.GetNumberOfFrames() * frameOffset_ > size)
     {
@@ -137,7 +144,7 @@
     const uint8_t* pixel = reinterpret_cast<const uint8_t*>(pixelData_) + 
       y * rowOffset_ + frame_ * frameOffset_;
 
-    // https://www.dabsoft.ch/dicom/3/C.7.6.3.1.3/
+    // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.3
     if (information_.IsPlanar())
     {
       /**
--- a/Core/DicomFormat/DicomIntegerPixelAccessor.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomIntegerPixelAccessor.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomMap.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomMap.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -35,7 +36,9 @@
 
 #include <stdio.h>
 #include <memory>
-#include "DicomArray.h"
+
+#include "../Endianness.h"
+#include "../Logging.h"
 #include "../OrthancException.h"
 
 
@@ -61,7 +64,11 @@
     DicomTag(0x0020, 0x0010),   // StudyID
     DICOM_TAG_STUDY_DESCRIPTION,
     DICOM_TAG_ACCESSION_NUMBER,
-    DICOM_TAG_STUDY_INSTANCE_UID
+    DICOM_TAG_STUDY_INSTANCE_UID,
+    DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION,   // New in db v6
+    DICOM_TAG_INSTITUTION_NAME,                  // New in db v6
+    DICOM_TAG_REQUESTING_PHYSICIAN,              // New in db v6
+    DICOM_TAG_REFERRING_PHYSICIAN_NAME           // New in db v6
   };
 
   static DicomTag seriesTags[] =
@@ -83,7 +90,12 @@
     DICOM_TAG_NUMBER_OF_SLICES,
     DICOM_TAG_NUMBER_OF_TIME_SLICES,
     DICOM_TAG_SERIES_INSTANCE_UID,
-    DICOM_TAG_IMAGE_ORIENTATION_PATIENT    // New in db v6
+    DICOM_TAG_IMAGE_ORIENTATION_PATIENT,                  // New in db v6
+    DICOM_TAG_SERIES_TYPE,                                // New in db v6
+    DICOM_TAG_OPERATOR_NAME,                              // New in db v6
+    DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION,       // New in db v6
+    DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION,  // New in db v6
+    DICOM_TAG_CONTRAST_BOLUS_AGENT                        // New in db v6
   };
 
   static DicomTag instanceTags[] =
@@ -96,7 +108,17 @@
     DICOM_TAG_NUMBER_OF_FRAMES,
     DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER,
     DICOM_TAG_SOP_INSTANCE_UID,
-    DICOM_TAG_IMAGE_POSITION_PATIENT    // New in db v6
+    DICOM_TAG_IMAGE_POSITION_PATIENT,    // New in db v6
+    DICOM_TAG_IMAGE_COMMENTS,            // New in db v6
+
+    /**
+     * Main DICOM tags that are not part of any release of the
+     * database schema yet, and that will be part of future db v7. In
+     * the meantime, the user must call "/tools/reconstruct" once to
+     * access these tags if the corresponding DICOM files where
+     * indexed in the database by an older version of Orthanc.
+     **/
+    DICOM_TAG_IMAGE_ORIENTATION_PATIENT  // New in Orthanc 1.4.2
   };
 
 
@@ -157,12 +179,11 @@
   }
 
 
-
-
   void DicomMap::Clear()
   {
     for (Map::iterator it = map_.begin(); it != map_.end(); ++it)
     {
+      assert(it->second != NULL);
       delete it->second;
     }
 
@@ -282,7 +303,7 @@
 
     for (size_t i = 0; i < count; i++)
     {
-      result.SetValue(tags[i], "");
+      result.SetValue(tags[i], "", false);
     }
   }
 
@@ -294,16 +315,22 @@
   void DicomMap::SetupFindStudyTemplate(DicomMap& result)
   {
     SetupFindTemplate(result, studyTags, sizeof(studyTags) / sizeof(DicomTag));
-    result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "");
-    result.SetValue(DICOM_TAG_PATIENT_ID, "");
+    result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false);
+    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
+
+    // These main DICOM tags are only indirectly related to the
+    // General Study Module, remove them
+    result.Remove(DICOM_TAG_INSTITUTION_NAME);
+    result.Remove(DICOM_TAG_REQUESTING_PHYSICIAN);
+    result.Remove(DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION);
   }
 
   void DicomMap::SetupFindSeriesTemplate(DicomMap& result)
   {
     SetupFindTemplate(result, seriesTags, sizeof(seriesTags) / sizeof(DicomTag));
-    result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "");
-    result.SetValue(DICOM_TAG_PATIENT_ID, "");
-    result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "");
+    result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false);
+    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
+    result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false);
 
     // These tags are considered as "main" by Orthanc, but are not in the Series module
     result.Remove(DicomTag(0x0008, 0x0070));  // Manufacturer
@@ -315,15 +342,18 @@
     result.Remove(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS);
     result.Remove(DICOM_TAG_NUMBER_OF_TIME_SLICES);
     result.Remove(DICOM_TAG_IMAGE_ORIENTATION_PATIENT);
+    result.Remove(DICOM_TAG_SERIES_TYPE);
+    result.Remove(DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION);
+    result.Remove(DICOM_TAG_CONTRAST_BOLUS_AGENT);
   }
 
   void DicomMap::SetupFindInstanceTemplate(DicomMap& result)
   {
     SetupFindTemplate(result, instanceTags, sizeof(instanceTags) / sizeof(DicomTag));
-    result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "");
-    result.SetValue(DICOM_TAG_PATIENT_ID, "");
-    result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "");
-    result.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "");
+    result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false);
+    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
+    result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false);
+    result.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "", false);
   }
 
 
@@ -443,13 +473,6 @@
   }
 
 
-  void DicomMap::Print(FILE* fp) const
-  {
-    DicomArray a(*this);
-    a.Print(fp);
-  }
-
-
   void DicomMap::GetTags(std::set<DicomTag>& tags) const
   {
     tags.clear();
@@ -460,4 +483,548 @@
       tags.insert(it->first);
     }
   }
+
+
+  static uint16_t ReadUnsignedInteger16(const char* dicom)
+  {
+    return le16toh(*reinterpret_cast<const uint16_t*>(dicom));
+  }
+
+
+  static uint32_t ReadUnsignedInteger32(const char* dicom)
+  {
+    return le32toh(*reinterpret_cast<const uint32_t*>(dicom));
+  }
+
+
+  static bool ValidateTag(const ValueRepresentation& vr,
+                          const std::string& value)
+  {
+    switch (vr)
+    {
+      case ValueRepresentation_ApplicationEntity:
+        return value.size() <= 16;
+
+      case ValueRepresentation_AgeString:
+        return (value.size() == 4 &&
+                isdigit(value[0]) &&
+                isdigit(value[1]) &&
+                isdigit(value[2]) &&
+                (value[3] == 'D' || value[3] == 'W' || value[3] == 'M' || value[3] == 'Y'));
+
+      case ValueRepresentation_AttributeTag:
+        return value.size() == 4;
+
+      case ValueRepresentation_CodeString:
+        return value.size() <= 16;
+
+      case ValueRepresentation_Date:
+        return value.size() <= 18;
+
+      case ValueRepresentation_DecimalString:
+        return value.size() <= 16;
+
+      case ValueRepresentation_DateTime:
+        return value.size() <= 54;
+
+      case ValueRepresentation_FloatingPointSingle:
+        return value.size() == 4;
+
+      case ValueRepresentation_FloatingPointDouble:
+        return value.size() == 8;
+
+      case ValueRepresentation_IntegerString:
+        return value.size() <= 12;
+
+      case ValueRepresentation_LongString:
+        return value.size() <= 64;
+
+      case ValueRepresentation_LongText:
+        return value.size() <= 10240;
+
+      case ValueRepresentation_OtherByte:
+        return true;
+      
+      case ValueRepresentation_OtherDouble:
+        return value.size() <= (static_cast<uint64_t>(1) << 32) - 8;
+
+      case ValueRepresentation_OtherFloat:
+        return value.size() <= (static_cast<uint64_t>(1) << 32) - 4;
+
+      case ValueRepresentation_OtherLong:
+        return true;
+
+      case ValueRepresentation_OtherWord:
+        return true;
+
+      case ValueRepresentation_PersonName:
+        return true;
+
+      case ValueRepresentation_ShortString:
+        return value.size() <= 16;
+
+      case ValueRepresentation_SignedLong:
+        return value.size() == 4;
+
+      case ValueRepresentation_Sequence:
+        return true;
+
+      case ValueRepresentation_SignedShort:
+        return value.size() == 2;
+
+      case ValueRepresentation_ShortText:
+        return value.size() <= 1024;
+
+      case ValueRepresentation_Time:
+        return value.size() <= 28;
+
+      case ValueRepresentation_UnlimitedCharacters:
+        return value.size() <= (static_cast<uint64_t>(1) << 32) - 2;
+
+      case ValueRepresentation_UniqueIdentifier:
+        return value.size() <= 64;
+
+      case ValueRepresentation_UnsignedLong:
+        return value.size() == 4;
+
+      case ValueRepresentation_Unknown:
+        return true;
+
+      case ValueRepresentation_UniversalResource:
+        return value.size() <= (static_cast<uint64_t>(1) << 32) - 2;
+
+      case ValueRepresentation_UnsignedShort:
+        return value.size() == 2;
+
+      case ValueRepresentation_UnlimitedText:
+        return value.size() <= (static_cast<uint64_t>(1) << 32) - 2;
+
+      default:
+        // Assume unsupported tags are OK
+        return true;
+    }
+  }
+
+
+  static void RemoveTagPadding(std::string& value,
+                               const ValueRepresentation& vr)
+  {
+    /**
+     * Remove padding from character strings, if need be. For the time
+     * being, only the UI VR is supported.
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
+     **/
+
+    switch (vr)
+    {
+      case ValueRepresentation_UniqueIdentifier:
+      {
+        /**
+         * "Values with a VR of UI shall be padded with a single
+         * trailing NULL (00H) character when necessary to achieve even
+         * length."
+         **/
+
+        if (!value.empty() &&
+            value[value.size() - 1] == '\0')
+        {
+          value.resize(value.size() - 1);
+        }
+
+        break;
+      }
+
+      /**
+       * TODO implement other VR
+       **/
+
+      default:
+        // No padding is applicable to this VR
+        break;
+    }
+  }
+
+
+  static bool ReadNextTag(DicomTag& tag,
+                          ValueRepresentation& vr,
+                          std::string& value,
+                          const char* dicom,
+                          size_t size,
+                          size_t& position)
+  {
+    /**
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_7.html#sect_7.1.2
+     * This function reads a data element with Explicit VR encoded using Little-Endian.
+     **/
+
+    if (position + 6 > size)
+    {
+      return false;
+    }
+
+    tag = DicomTag(ReadUnsignedInteger16(dicom + position),
+                   ReadUnsignedInteger16(dicom + position + 2));
+
+    vr = StringToValueRepresentation(std::string(dicom + position + 4, 2), true);
+    if (vr == ValueRepresentation_NotSupported)
+    {
+      return false;
+    }
+
+    if (vr == ValueRepresentation_OtherByte ||
+        vr == ValueRepresentation_OtherDouble ||
+        vr == ValueRepresentation_OtherFloat ||
+        vr == ValueRepresentation_OtherLong ||
+        vr == ValueRepresentation_OtherWord ||
+        vr == ValueRepresentation_Sequence ||
+        vr == ValueRepresentation_UnlimitedCharacters ||
+        vr == ValueRepresentation_UniversalResource ||
+        vr == ValueRepresentation_UnlimitedText ||
+        vr == ValueRepresentation_Unknown)    // Note that "UN" should never appear in the Meta Information
+    {
+      if (position + 12 > size)
+      {
+        return false;
+      }
+
+      uint32_t length = ReadUnsignedInteger32(dicom + position + 8);
+
+      if (position + 12 + length > size)
+      {
+        return false;
+      }
+
+      value.assign(dicom + position + 12, length);
+      position += (12 + length);
+    }
+    else
+    {
+      if (position + 8 > size)
+      {
+        return false;
+      }
+
+      uint16_t length = ReadUnsignedInteger16(dicom + position + 6);
+
+      if (position + 8 + length > size)
+      {
+        return false;
+      }
+
+      value.assign(dicom + position + 8, length);
+      position += (8 + length);
+    }
+
+    if (!ValidateTag(vr, value))
+    {
+      return false;
+    }
+
+    RemoveTagPadding(value, vr);
+
+    return true;
+  }
+
+
+  bool DicomMap::ParseDicomMetaInformation(DicomMap& result,
+                                           const char* dicom,
+                                           size_t size)
+  {
+    /**
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part10/chapter_7.html
+     * According to Table 7.1-1, besides the "DICM" DICOM prefix, the
+     * file preamble (i.e. dicom[0..127]) should not be taken into
+     * account to determine whether the file is or is not a DICOM file.
+     **/
+
+    if (size < 132 ||
+        dicom[128] != 'D' ||
+        dicom[129] != 'I' ||
+        dicom[130] != 'C' ||
+        dicom[131] != 'M')
+    {
+      return false;
+    }
+
+
+    /**
+     * The DICOM File Meta Information must be encoded using the
+     * Explicit VR Little Endian Transfer Syntax
+     * (UID=1.2.840.10008.1.2.1).
+     **/
+
+    result.Clear();
+
+    // First, we read the "File Meta Information Group Length" tag
+    // (0002,0000) to know where to stop reading the meta header
+    size_t position = 132;
+
+    DicomTag tag(0x0000, 0x0000);  // Dummy initialization
+    ValueRepresentation vr;
+    std::string value;
+    if (!ReadNextTag(tag, vr, value, dicom, size, position) ||
+        tag.GetGroup() != 0x0002 ||
+        tag.GetElement() != 0x0000 ||
+        vr != ValueRepresentation_UnsignedLong ||
+        value.size() != 4)
+    {
+      return false;
+    }
+
+    size_t stopPosition = position + ReadUnsignedInteger32(value.c_str());
+    if (stopPosition > size)
+    {
+      return false;
+    }
+
+    while (position < stopPosition)
+    {
+      if (ReadNextTag(tag, vr, value, dicom, size, position))
+      {
+        result.SetValue(tag, value, IsBinaryValueRepresentation(vr));
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  static std::string ValueAsString(const DicomMap& summary,
+                                   const DicomTag& tag)
+  {
+    const DicomValue& value = summary.GetValue(tag);
+    if (value.IsNull())
+    {
+      return "(null)";
+    }
+    else
+    {
+      return value.GetContent();
+    }
+  }
+
+
+  void DicomMap::LogMissingTagsForStore() const
+  {
+    std::string s, t;
+
+    if (HasTag(DICOM_TAG_PATIENT_ID))
+    {
+      if (t.size() > 0)
+        t += ", ";
+      t += "PatientID=" + ValueAsString(*this, DICOM_TAG_PATIENT_ID);
+    }
+    else
+    {
+      if (s.size() > 0)
+        s += ", ";
+      s += "PatientID";
+    }
+
+    if (HasTag(DICOM_TAG_STUDY_INSTANCE_UID))
+    {
+      if (t.size() > 0)
+        t += ", ";
+      t += "StudyInstanceUID=" + ValueAsString(*this, DICOM_TAG_STUDY_INSTANCE_UID);
+    }
+    else
+    {
+      if (s.size() > 0)
+        s += ", ";
+      s += "StudyInstanceUID";
+    }
+
+    if (HasTag(DICOM_TAG_SERIES_INSTANCE_UID))
+    {
+      if (t.size() > 0)
+        t += ", ";
+      t += "SeriesInstanceUID=" + ValueAsString(*this, DICOM_TAG_SERIES_INSTANCE_UID);
+    }
+    else
+    {
+      if (s.size() > 0)
+        s += ", ";
+      s += "SeriesInstanceUID";
+    }
+
+    if (HasTag(DICOM_TAG_SOP_INSTANCE_UID))
+    {
+      if (t.size() > 0)
+        t += ", ";
+      t += "SOPInstanceUID=" + ValueAsString(*this, DICOM_TAG_SOP_INSTANCE_UID);
+    }
+    else
+    {
+      if (s.size() > 0)
+        s += ", ";
+      s += "SOPInstanceUID";
+    }
+
+    if (t.size() == 0)
+    {
+      LOG(ERROR) << "Store has failed because all the required tags (" << s << ") are missing (is it a DICOMDIR file?)";
+    }
+    else
+    {
+      LOG(ERROR) << "Store has failed because required tags (" << s << ") are missing for the following instance: " << t;
+    }
+  }
+
+
+  bool DicomMap::CopyToString(std::string& result,
+                              const DicomTag& tag,
+                              bool allowBinary) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->CopyToString(result, allowBinary);
+    }
+  }
+    
+  bool DicomMap::ParseInteger32(int32_t& result,
+                                const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseInteger32(result);
+    }
+  }
+
+  bool DicomMap::ParseInteger64(int64_t& result,
+                                const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseInteger64(result);
+    }
+  }
+
+  bool DicomMap::ParseUnsignedInteger32(uint32_t& result,
+                                        const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseUnsignedInteger32(result);
+    }
+  }
+
+  bool DicomMap::ParseUnsignedInteger64(uint64_t& result,
+                                        const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseUnsignedInteger64(result);
+    }
+  }
+
+  bool DicomMap::ParseFloat(float& result,
+                            const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseFloat(result);
+    }
+  }
+
+  bool DicomMap::ParseDouble(double& result,
+                             const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseDouble(result);
+    }
+  }
+
+  
+  void DicomMap::Serialize(Json::Value& target) const
+  {
+    target = Json::objectValue;
+
+    for (Map::const_iterator it = map_.begin(); it != map_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      
+      std::string tag = it->first.Format();
+
+      Json::Value value;
+      it->second->Serialize(value);
+
+      target[tag] = value;
+    }
+  }
+  
+
+  void DicomMap::Unserialize(const Json::Value& source)
+  {
+    Clear();
+
+    if (source.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    Json::Value::Members tags = source.getMemberNames();
+
+    for (size_t i = 0; i < tags.size(); i++)
+    {
+      DicomTag tag(0, 0);
+      
+      if (!DicomTag::ParseHexadecimal(tag, tags[i].c_str()) ||
+          map_.find(tag) != map_.end())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      std::auto_ptr<DicomValue> value(new DicomValue);
+      value->Unserialize(source[tags[i]]);
+
+      map_[tag] = value.release();
+    }
+  }
 }
--- a/Core/DicomFormat/DicomMap.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomMap.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -47,7 +48,7 @@
   private:
     friend class DicomArray;
     friend class FromDcmtkBridge;
-    friend class ToDcmtkBridge;
+    friend class ParsedDicomFile;
 
     typedef std::map<DicomTag, DicomValue*>  Map;
 
@@ -88,6 +89,17 @@
 
     void Clear();
 
+    void SetNullValue(uint16_t group, 
+                      uint16_t element)
+    {
+      SetValue(group, element, new DicomValue);
+    }
+    
+    void SetNullValue(const DicomTag& tag)
+    {
+      SetValue(tag, new DicomValue);
+    }
+    
     void SetValue(uint16_t group, 
                   uint16_t element, 
                   const DicomValue& value)
@@ -102,16 +114,18 @@
     }
 
     void SetValue(const DicomTag& tag,
-                  const std::string& str)
+                  const std::string& str,
+                  bool isBinary)
     {
-      SetValue(tag, new DicomValue(str, false));
+      SetValue(tag, new DicomValue(str, isBinary));
     }
 
     void SetValue(uint16_t group, 
                   uint16_t element, 
-                  const std::string& str)
+                  const std::string& str,
+                  bool isBinary)
     {
-      SetValue(group, element, new DicomValue(str, false));
+      SetValue(group, element, new DicomValue(str, isBinary));
     }
 
     bool HasTag(uint16_t group, uint16_t element) const
@@ -169,12 +183,42 @@
 
     static void GetMainDicomTags(std::set<DicomTag>& result);
 
-    void Print(FILE* fp) const;
-
     void GetTags(std::set<DicomTag>& tags) const;
 
     static void LoadMainDicomTags(const DicomTag*& tags,
                                   size_t& size,
                                   ResourceType level);
+
+    static bool ParseDicomMetaInformation(DicomMap& result,
+                                          const char* dicom,
+                                          size_t size);
+
+    void LogMissingTagsForStore() const;
+
+    bool CopyToString(std::string& result,
+                      const DicomTag& tag,
+                      bool allowBinary) const;
+    
+    bool ParseInteger32(int32_t& result,
+                        const DicomTag& tag) const;
+
+    bool ParseInteger64(int64_t& result,
+                        const DicomTag& tag) const;                                
+
+    bool ParseUnsignedInteger32(uint32_t& result,
+                                const DicomTag& tag) const;
+
+    bool ParseUnsignedInteger64(uint64_t& result,
+                                const DicomTag& tag) const;
+
+    bool ParseFloat(float& result,
+                    const DicomTag& tag) const;
+
+    bool ParseDouble(double& result,
+                     const DicomTag& tag) const;
+
+    void Serialize(Json::Value& target) const;
+
+    void Unserialize(const Json::Value& source);
   };
 }
--- a/Core/DicomFormat/DicomTag.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomTag.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -38,9 +39,32 @@
 #include <iostream>
 #include <iomanip>
 #include <stdio.h>
+#include <string.h>
 
 namespace Orthanc
-{
+{ 
+  static inline uint16_t GetCharValue(char c)
+  {
+    if (c >= '0' && c <= '9')
+      return c - '0';
+    else if (c >= 'a' && c <= 'f')
+      return c - 'a' + 10;
+    else if (c >= 'A' && c <= 'F')
+      return c - 'A' + 10;
+    else
+      return 0;
+  }
+
+
+  static inline uint16_t GetTagValue(const char* c)
+  {
+    return ((GetCharValue(c[0]) << 12) + 
+            (GetCharValue(c[1]) << 8) + 
+            (GetCharValue(c[2]) << 4) + 
+            GetCharValue(c[3]));
+  }
+
+
   bool DicomTag::operator< (const DicomTag& other) const
   {
     if (group_ < other.group_)
@@ -73,6 +97,49 @@
   }
 
 
+  bool DicomTag::ParseHexadecimal(DicomTag& tag,
+                                  const char* value)
+  {
+    size_t length = strlen(value);
+
+    if (length == 9 &&
+        isxdigit(value[0]) &&
+        isxdigit(value[1]) &&
+        isxdigit(value[2]) &&
+        isxdigit(value[3]) &&
+        (value[4] == '-' || value[4] == ',') &&
+        isxdigit(value[5]) &&
+        isxdigit(value[6]) &&
+        isxdigit(value[7]) &&
+        isxdigit(value[8]))        
+    {
+      uint16_t group = GetTagValue(value);
+      uint16_t element = GetTagValue(value + 5);
+      tag = DicomTag(group, element);
+      return true;
+    }
+    else if (length == 8 &&
+             isxdigit(value[0]) &&
+             isxdigit(value[1]) &&
+             isxdigit(value[2]) &&
+             isxdigit(value[3]) &&
+             isxdigit(value[4]) &&
+             isxdigit(value[5]) &&
+             isxdigit(value[6]) &&
+             isxdigit(value[7])) 
+    {
+      uint16_t group = GetTagValue(value);
+      uint16_t element = GetTagValue(value + 4);
+      tag = DicomTag(group, element);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
   const char* DicomTag::GetMainTagsName() const
   {
     if (*this == DICOM_TAG_ACCESSION_NUMBER)
--- a/Core/DicomFormat/DicomTag.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomTag.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -66,6 +67,11 @@
       return element_;
     }
 
+    bool IsPrivate() const
+    {
+      return group_ % 2 == 1;
+    }
+
     const char* GetMainTagsName() const;
 
     bool operator< (const DicomTag& other) const;
@@ -82,6 +88,9 @@
 
     std::string Format() const;
 
+    static bool ParseHexadecimal(DicomTag& tag,
+                                 const char* value);
+
     friend std::ostream& operator<< (std::ostream& o, const DicomTag& tag);
 
     static void AddTagsForModule(std::set<DicomTag>& target,
@@ -95,6 +104,7 @@
   static const DicomTag DICOM_TAG_SERIES_INSTANCE_UID(0x0020, 0x000e);
   static const DicomTag DICOM_TAG_STUDY_INSTANCE_UID(0x0020, 0x000d);
   static const DicomTag DICOM_TAG_PIXEL_DATA(0x7fe0, 0x0010);
+  static const DicomTag DICOM_TAG_TRANSFER_SYNTAX_UID(0x0002, 0x0010);
 
   static const DicomTag DICOM_TAG_IMAGE_INDEX(0x0054, 0x1330);
   static const DicomTag DICOM_TAG_INSTANCE_NUMBER(0x0020, 0x0013);
@@ -122,6 +132,7 @@
   static const DicomTag DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER(0x0020, 0x0100);
 
   // Tags for C-FIND and C-MOVE
+  static const DicomTag DICOM_TAG_MESSAGE_ID(0x0000, 0x0110);
   static const DicomTag DICOM_TAG_SPECIFIC_CHARACTER_SET(0x0008, 0x0005);
   static const DicomTag DICOM_TAG_QUERY_RETRIEVE_LEVEL(0x0008, 0x0052);
   static const DicomTag DICOM_TAG_MODALITIES_IN_STUDY(0x0008, 0x0061);
@@ -152,4 +163,47 @@
   static const DicomTag DICOM_TAG_SERIES_TIME(0x0008, 0x0031);
   static const DicomTag DICOM_TAG_STUDY_DATE(0x0008, 0x0020);
   static const DicomTag DICOM_TAG_STUDY_TIME(0x0008, 0x0030);
+
+  // Various tags
+  static const DicomTag DICOM_TAG_SERIES_TYPE(0x0054, 0x1000);
+  static const DicomTag DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION(0x0032, 0x1060);
+  static const DicomTag DICOM_TAG_INSTITUTION_NAME(0x0008, 0x0080);
+  static const DicomTag DICOM_TAG_REQUESTING_PHYSICIAN(0x0032, 0x1032);
+  static const DicomTag DICOM_TAG_REFERRING_PHYSICIAN_NAME(0x0008, 0x0090);
+  static const DicomTag DICOM_TAG_OPERATOR_NAME(0x0008, 0x1070);
+  static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION(0x0040, 0x0254);
+  static const DicomTag DICOM_TAG_IMAGE_COMMENTS(0x0020, 0x4000);
+  static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION(0x0018, 0x1400);
+  static const DicomTag DICOM_TAG_CONTRAST_BOLUS_AGENT(0x0018, 0x0010);
+
+  // Tags used within the Stone of Orthanc
+  static const DicomTag DICOM_TAG_FRAME_INCREMENT_POINTER(0x0028, 0x0009);
+  static const DicomTag DICOM_TAG_GRID_FRAME_OFFSET_VECTOR(0x3004, 0x000c);
+  static const DicomTag DICOM_TAG_PIXEL_SPACING(0x0028, 0x0030);
+  static const DicomTag DICOM_TAG_RESCALE_INTERCEPT(0x0028, 0x1052);
+  static const DicomTag DICOM_TAG_RESCALE_SLOPE(0x0028, 0x1053);
+  static const DicomTag DICOM_TAG_SLICE_THICKNESS(0x0018, 0x0050);
+  static const DicomTag DICOM_TAG_WINDOW_CENTER(0x0028, 0x1050);
+  static const DicomTag DICOM_TAG_WINDOW_WIDTH(0x0028, 0x1051);
+  static const DicomTag DICOM_TAG_DOSE_GRID_SCALING(0x3004, 0x000e);
+                                    
+  // Counting patients, studies and series
+  // https://www.medicalconnections.co.uk/kb/Counting_Studies_Series_and_Instances
+  static const DicomTag DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES(0x0020, 0x1200);  
+  static const DicomTag DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES(0x0020, 0x1202);  
+  static const DicomTag DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES(0x0020, 0x1204);  
+  static const DicomTag DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES(0x0020, 0x1206);  
+  static const DicomTag DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES(0x0020, 0x1208);  
+  static const DicomTag DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES(0x0020, 0x1209);  
+  static const DicomTag DICOM_TAG_SOP_CLASSES_IN_STUDY(0x0008, 0x0062);  
+
+  // Tags to preserve relationships during anonymization
+  static const DicomTag DICOM_TAG_REFERENCED_IMAGE_SEQUENCE(0x0008, 0x1140);
+  static const DicomTag DICOM_TAG_REFERENCED_SOP_INSTANCE_UID(0x0008, 0x1155);
+  static const DicomTag DICOM_TAG_SOURCE_IMAGE_SEQUENCE(0x0008, 0x2112);
+  static const DicomTag DICOM_TAG_FRAME_OF_REFERENCE_UID(0x0020, 0x0052);
+  static const DicomTag DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_UID(0x3006, 0x0024);
+  static const DicomTag DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID(0x3006, 0x00c2);
+  static const DicomTag DICOM_TAG_CURRENT_REQUESTED_PROCEDURE_EVIDENCE_SEQUENCE(0x0040, 0xa375);
+  static const DicomTag DICOM_TAG_REFERENCED_SERIES_SEQUENCE(0x0008, 0x1115);
 }
--- a/Core/DicomFormat/DicomValue.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomValue.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,8 +35,11 @@
 #include "DicomValue.h"
 
 #include "../OrthancException.h"
+#include "../SerializationToolbox.h"
 #include "../Toolbox.h"
 
+#include <boost/lexical_cast.hpp>
+
 namespace Orthanc
 {
   DicomValue::DicomValue(const DicomValue& other) : 
@@ -81,7 +85,7 @@
   }
 
   
-#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1
+#if ORTHANC_ENABLE_BASE64 == 1
   void DicomValue::FormatDataUriScheme(std::string& target,
                                        const std::string& mime) const
   {
@@ -90,4 +94,165 @@
   }
 #endif
 
+
+  template <typename T,
+            bool allowSigned>
+  static bool ParseValue(T& result,
+                         const DicomValue& source)
+  {
+    if (source.IsBinary() ||
+        source.IsNull())
+    {
+      return false;
+    }
+    
+    try
+    {
+      std::string value = Toolbox::StripSpaces(source.GetContent());
+      if (value.empty())
+      {
+        return false;
+      }
+
+      if (!allowSigned &&
+          value[0] == '-')
+      {
+        return false;
+      }
+      
+      result = boost::lexical_cast<T>(value);
+      return true;
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      return false;
+    }
+  }
+
+  bool DicomValue::ParseInteger32(int32_t& result) const
+  {
+    int64_t tmp;
+    if (ParseValue<int64_t, true>(tmp, *this))
+    {
+      result = static_cast<int32_t>(tmp);
+      return (tmp == static_cast<int64_t>(result));  // Check no overflow occurs
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  bool DicomValue::ParseInteger64(int64_t& result) const
+  {
+    return ParseValue<int64_t, true>(result, *this);
+  }
+
+  bool DicomValue::ParseUnsignedInteger32(uint32_t& result) const
+  {
+    uint64_t tmp;
+    if (ParseValue<uint64_t, false>(tmp, *this))
+    {
+      result = static_cast<uint32_t>(tmp);
+      return (tmp == static_cast<uint64_t>(result));  // Check no overflow occurs
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  bool DicomValue::ParseUnsignedInteger64(uint64_t& result) const
+  {
+    return ParseValue<uint64_t, false>(result, *this);
+  }
+
+  bool DicomValue::ParseFloat(float& result) const
+  {
+    return ParseValue<float, true>(result, *this);
+  }
+
+  bool DicomValue::ParseDouble(double& result) const
+  {
+    return ParseValue<double, true>(result, *this);
+  }
+
+  bool DicomValue::CopyToString(std::string& result,
+                                bool allowBinary) const
+  {
+    if (IsNull())
+    {
+      return false;
+    }
+    else if (IsBinary() && !allowBinary)
+    {
+      return false;
+    }
+    else
+    {
+      result.assign(content_);
+      return true;
+    }
+  }    
+
+
+  static const char* KEY_TYPE = "Type";
+  static const char* KEY_CONTENT = "Content";
+  
+  void DicomValue::Serialize(Json::Value& target) const
+  {
+    target = Json::objectValue;
+
+    switch (type_)
+    {
+      case Type_Null:
+        target[KEY_TYPE] = "Null";
+        break;
+
+      case Type_String:
+        target[KEY_TYPE] = "String";
+        target[KEY_CONTENT] = content_;
+        break;
+
+      case Type_Binary:
+      {
+        target[KEY_TYPE] = "Binary";
+
+        std::string base64;
+        Toolbox::EncodeBase64(base64, content_);
+        target[KEY_CONTENT] = base64;
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+  void DicomValue::Unserialize(const Json::Value& source)
+  {
+    std::string type = SerializationToolbox::ReadString(source, KEY_TYPE);
+
+    if (type == "Null")
+    {
+      type_ = Type_Null;
+      content_.clear();
+    }
+    else if (type == "String")
+    {
+      type_ = Type_String;
+      content_ = SerializationToolbox::ReadString(source, KEY_CONTENT);
+    }
+    else if (type == "Binary")
+    {
+      type_ = Type_Binary;
+
+      const std::string base64 =SerializationToolbox::ReadString(source, KEY_CONTENT);
+      Toolbox::DecodeBase64(content_, base64);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
 }
--- a/Core/DicomFormat/DicomValue.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/DicomFormat/DicomValue.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,8 +33,15 @@
 
 #pragma once
 
+#include <stdint.h>
 #include <string>
 #include <boost/noncopyable.hpp>
+#include <json/value.h>
+
+#if !defined(ORTHANC_ENABLE_BASE64)
+#  error The macro ORTHANC_ENABLE_BASE64 must be defined
+#endif
+
 
 namespace Orthanc
 {
@@ -78,7 +86,7 @@
     
     DicomValue* Clone() const;
 
-#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1
+#if ORTHANC_ENABLE_BASE64 == 1
     void FormatDataUriScheme(std::string& target,
                              const std::string& mime) const;
 
@@ -87,5 +95,24 @@
       FormatDataUriScheme(target, "application/octet-stream");
     }
 #endif
+
+    bool CopyToString(std::string& result,
+                      bool allowBinary) const;
+    
+    bool ParseInteger32(int32_t& result) const;
+
+    bool ParseInteger64(int64_t& result) const;                                
+
+    bool ParseUnsignedInteger32(uint32_t& result) const;
+
+    bool ParseUnsignedInteger64(uint64_t& result) const;                                
+
+    bool ParseFloat(float& result) const;                                
+
+    bool ParseDouble(double& result) const;
+
+    void Serialize(Json::Value& target) const;
+
+    void Unserialize(const Json::Value& source);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomFindAnswers.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,178 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomFindAnswers.h"
+
+#include "../DicomParsing/FromDcmtkBridge.h"
+#include "../OrthancException.h"
+
+#include <memory>
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <boost/noncopyable.hpp>
+
+
+namespace Orthanc
+{
+  void DicomFindAnswers::AddAnswerInternal(ParsedDicomFile* answer)
+  {
+    std::auto_ptr<ParsedDicomFile> protection(answer);
+
+    if (isWorklist_)
+    {
+      // These lines are necessary when serving worklists, otherwise
+      // Orthanc does not behave as "wlmscpfs"
+      protection->Remove(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID);
+      protection->Remove(DICOM_TAG_SOP_INSTANCE_UID);
+    }
+
+    protection->ChangeEncoding(encoding_);
+
+    answers_.push_back(protection.release());
+  }
+
+
+  DicomFindAnswers::DicomFindAnswers(bool isWorklist) : 
+    encoding_(GetDefaultDicomEncoding()),
+    isWorklist_(isWorklist),
+    complete_(true)
+  {
+  }
+
+
+  void DicomFindAnswers::SetEncoding(Encoding encoding)
+  {
+    for (size_t i = 0; i < answers_.size(); i++)
+    {
+      assert(answers_[i] != NULL);
+      answers_[i]->ChangeEncoding(encoding);
+    }
+
+    encoding_ = encoding;
+  }
+
+
+  void DicomFindAnswers::SetWorklist(bool isWorklist)
+  {
+    if (answers_.empty())
+    {
+      isWorklist_ = isWorklist;
+    }
+    else
+    {
+      // This set of answers is not empty anymore, cannot change its type
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void DicomFindAnswers::Clear()
+  {
+    for (size_t i = 0; i < answers_.size(); i++)
+    {
+      assert(answers_[i] != NULL);
+      delete answers_[i];
+    }
+
+    answers_.clear();
+  }
+
+
+  void DicomFindAnswers::Reserve(size_t size)
+  {
+    if (size > answers_.size())
+    {
+      answers_.reserve(size);
+    }
+  }
+
+
+  void DicomFindAnswers::Add(const DicomMap& map)
+  {
+    AddAnswerInternal(new ParsedDicomFile(map, encoding_));
+  }
+
+
+  void DicomFindAnswers::Add(ParsedDicomFile& dicom)
+  {
+    AddAnswerInternal(dicom.Clone(true));
+  }
+
+  void DicomFindAnswers::Add(const void* dicom,
+                             size_t size)
+  {
+    AddAnswerInternal(new ParsedDicomFile(dicom, size));
+  }
+
+
+  ParsedDicomFile& DicomFindAnswers::GetAnswer(size_t index) const
+  {
+    if (index < answers_.size())
+    {
+      return *answers_[index];
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  DcmDataset* DicomFindAnswers::ExtractDcmDataset(size_t index) const
+  {
+    return new DcmDataset(*GetAnswer(index).GetDcmtkObject().getDataset());
+  }
+
+
+  void DicomFindAnswers::ToJson(Json::Value& target,
+                                size_t index,
+                                bool simplify) const
+  {
+    DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Human : DicomToJsonFormat_Full);
+    GetAnswer(index).DatasetToJson(target, format, DicomToJsonFlags_None, 0);
+  }
+
+
+  void DicomFindAnswers::ToJson(Json::Value& target,
+                                bool simplify) const
+  {
+    target = Json::arrayValue;
+
+    for (size_t i = 0; i < GetSize(); i++)
+    {
+      Json::Value answer;
+      ToJson(answer, i, simplify);
+      target.append(answer);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomFindAnswers.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../DicomParsing/ParsedDicomFile.h"
+
+namespace Orthanc
+{
+  class DicomFindAnswers : public boost::noncopyable
+  {
+  private:
+    Encoding                      encoding_;
+    bool                          isWorklist_;
+    std::vector<ParsedDicomFile*> answers_;
+    bool                          complete_;
+
+    void AddAnswerInternal(ParsedDicomFile* answer);
+
+  public:
+    DicomFindAnswers(bool isWorklist);
+
+    ~DicomFindAnswers()
+    {
+      Clear();
+    }
+
+    Encoding GetEncoding() const
+    {
+      return encoding_;
+    }
+
+    void SetEncoding(Encoding encoding);
+
+    void SetWorklist(bool isWorklist);
+
+    bool IsWorklist() const
+    {
+      return isWorklist_;
+    }
+
+    void Clear();
+
+    void Reserve(size_t index);
+
+    void Add(const DicomMap& map);
+
+    void Add(ParsedDicomFile& dicom);
+
+    void Add(const void* dicom,
+             size_t size);
+
+    size_t GetSize() const
+    {
+      return answers_.size();
+    }
+
+    ParsedDicomFile& GetAnswer(size_t index) const;
+
+    DcmDataset* ExtractDcmDataset(size_t index) const;
+
+    void ToJson(Json::Value& target,
+                bool simplify) const;
+
+    void ToJson(Json::Value& target,
+                size_t index,
+                bool simplify) const;
+
+    bool IsComplete() const
+    {
+      return complete_;
+    }
+
+    void SetComplete(bool isComplete)
+    {
+      complete_ = isComplete;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomServer.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,382 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomServer.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/MultiThreading/RunnableWorkersPool.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/Toolbox.h"
+#include "Internals/CommandDispatcher.h"
+
+#include <boost/thread.hpp>
+
+#if defined(__linux__)
+#include <cstdlib>
+#endif
+
+
+namespace Orthanc
+{
+  struct DicomServer::PImpl
+  {
+    boost::thread  thread_;
+    T_ASC_Network *network_;
+    std::auto_ptr<RunnableWorkersPool>  workers_;
+  };
+
+
+  void DicomServer::ServerThread(DicomServer* server)
+  {
+    LOG(INFO) << "DICOM server started";
+
+    while (server->continue_)
+    {
+      /* receive an association and acknowledge or reject it. If the association was */
+      /* acknowledged, offer corresponding services and invoke one or more if required. */
+      std::auto_ptr<Internals::CommandDispatcher> dispatcher(Internals::AcceptAssociation(*server, server->pimpl_->network_));
+
+      try
+      {
+        if (dispatcher.get() != NULL)
+        {
+          server->pimpl_->workers_->Add(dispatcher.release());
+        }
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Exception in the DICOM server thread: " << e.What();
+      }
+    }
+
+    LOG(INFO) << "DICOM server stopping";
+  }
+
+
+  DicomServer::DicomServer() : 
+    pimpl_(new PImpl),
+    aet_("ANY-SCP")
+  {
+    port_ = 104;
+    modalities_ = NULL;
+    findRequestHandlerFactory_ = NULL;
+    moveRequestHandlerFactory_ = NULL;
+    storeRequestHandlerFactory_ = NULL;
+    worklistRequestHandlerFactory_ = NULL;
+    applicationEntityFilter_ = NULL;
+    checkCalledAet_ = true;
+    associationTimeout_ = 30;
+    continue_ = false;
+  }
+
+  DicomServer::~DicomServer()
+  {
+    if (continue_)
+    {
+      LOG(ERROR) << "INTERNAL ERROR: DicomServer::Stop() should be invoked manually to avoid mess in the destruction order!";
+      Stop();
+    }
+  }
+
+  void DicomServer::SetPortNumber(uint16_t port)
+  {
+    Stop();
+    port_ = port;
+  }
+
+  uint16_t DicomServer::GetPortNumber() const
+  {
+    return port_;
+  }
+
+  void DicomServer::SetAssociationTimeout(uint32_t seconds)
+  {
+    LOG(INFO) << "Setting timeout for DICOM connections if Orthanc acts as SCP (server): " 
+              << seconds << " seconds (0 = no timeout)";
+
+    Stop();
+    associationTimeout_ = seconds;
+  }
+
+  uint32_t DicomServer::GetAssociationTimeout() const
+  {
+    return associationTimeout_;
+  }
+
+
+  void DicomServer::SetCalledApplicationEntityTitleCheck(bool check)
+  {
+    Stop();
+    checkCalledAet_ = check;
+  }
+
+  bool DicomServer::HasCalledApplicationEntityTitleCheck() const
+  {
+    return checkCalledAet_;
+  }
+
+  void DicomServer::SetApplicationEntityTitle(const std::string& aet)
+  {
+    if (aet.size() == 0)
+    {
+      throw OrthancException(ErrorCode_BadApplicationEntityTitle);
+    }
+
+    if (aet.size() > 16)
+    {
+      throw OrthancException(ErrorCode_BadApplicationEntityTitle);
+    }
+
+    for (size_t i = 0; i < aet.size(); i++)
+    {
+      if (!(aet[i] == '-' ||
+            aet[i] == '_' ||
+            isdigit(aet[i]) ||
+            (aet[i] >= 'A' && aet[i] <= 'Z')))
+      {
+        LOG(WARNING) << "For best interoperability, only upper case, alphanumeric characters should be present in AET: \"" << aet << "\"";
+        break;
+      }
+    }
+
+    Stop();
+    aet_ = aet;
+  }
+
+  const std::string& DicomServer::GetApplicationEntityTitle() const
+  {
+    return aet_;
+  }
+
+  void DicomServer::SetRemoteModalities(IRemoteModalities& modalities)
+  {
+    Stop();
+    modalities_ = &modalities;
+  }
+  
+  DicomServer::IRemoteModalities& DicomServer::GetRemoteModalities() const
+  {
+    if (modalities_ == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *modalities_;
+    }
+  }
+    
+  void DicomServer::SetFindRequestHandlerFactory(IFindRequestHandlerFactory& factory)
+  {
+    Stop();
+    findRequestHandlerFactory_ = &factory;
+  }
+
+  bool DicomServer::HasFindRequestHandlerFactory() const
+  {
+    return (findRequestHandlerFactory_ != NULL);
+  }
+
+  IFindRequestHandlerFactory& DicomServer::GetFindRequestHandlerFactory() const
+  {
+    if (HasFindRequestHandlerFactory())
+    {
+      return *findRequestHandlerFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoCFindHandler);
+    }
+  }
+
+  void DicomServer::SetMoveRequestHandlerFactory(IMoveRequestHandlerFactory& factory)
+  {
+    Stop();
+    moveRequestHandlerFactory_ = &factory;
+  }
+
+  bool DicomServer::HasMoveRequestHandlerFactory() const
+  {
+    return (moveRequestHandlerFactory_ != NULL);
+  }
+
+  IMoveRequestHandlerFactory& DicomServer::GetMoveRequestHandlerFactory() const
+  {
+    if (HasMoveRequestHandlerFactory())
+    {
+      return *moveRequestHandlerFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoCMoveHandler);
+    }
+  }
+
+  void DicomServer::SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& factory)
+  {
+    Stop();
+    storeRequestHandlerFactory_ = &factory;
+  }
+
+  bool DicomServer::HasStoreRequestHandlerFactory() const
+  {
+    return (storeRequestHandlerFactory_ != NULL);
+  }
+
+  IStoreRequestHandlerFactory& DicomServer::GetStoreRequestHandlerFactory() const
+  {
+    if (HasStoreRequestHandlerFactory())
+    {
+      return *storeRequestHandlerFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoCStoreHandler);
+    }
+  }
+
+  void DicomServer::SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& factory)
+  {
+    Stop();
+    worklistRequestHandlerFactory_ = &factory;
+  }
+
+  bool DicomServer::HasWorklistRequestHandlerFactory() const
+  {
+    return (worklistRequestHandlerFactory_ != NULL);
+  }
+
+  IWorklistRequestHandlerFactory& DicomServer::GetWorklistRequestHandlerFactory() const
+  {
+    if (HasWorklistRequestHandlerFactory())
+    {
+      return *worklistRequestHandlerFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoWorklistHandler);
+    }
+  }
+
+  void DicomServer::SetApplicationEntityFilter(IApplicationEntityFilter& factory)
+  {
+    Stop();
+    applicationEntityFilter_ = &factory;
+  }
+
+  bool DicomServer::HasApplicationEntityFilter() const
+  {
+    return (applicationEntityFilter_ != NULL);
+  }
+
+  IApplicationEntityFilter& DicomServer::GetApplicationEntityFilter() const
+  {
+    if (HasApplicationEntityFilter())
+    {
+      return *applicationEntityFilter_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoApplicationEntityFilter);
+    }
+  }
+
+  void DicomServer::Start()
+  {
+    if (modalities_ == NULL)
+    {
+      LOG(ERROR) << "No list of modalities was provided to the DICOM server";
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    
+    Stop();
+
+    /* initialize network, i.e. create an instance of T_ASC_Network*. */
+    OFCondition cond = ASC_initializeNetwork
+      (NET_ACCEPTOR, OFstatic_cast(int, port_), /*opt_acse_timeout*/ 30, &pimpl_->network_);
+    if (cond.bad())
+    {
+      LOG(ERROR) << "cannot create network: " << cond.text();
+      throw OrthancException(ErrorCode_DicomPortInUse);
+    }
+
+    continue_ = true;
+    pimpl_->workers_.reset(new RunnableWorkersPool(4));   // Use 4 workers - TODO as a parameter?
+    pimpl_->thread_ = boost::thread(ServerThread, this);
+  }
+
+
+  void DicomServer::Stop()
+  {
+    if (continue_)
+    {
+      continue_ = false;
+
+      if (pimpl_->thread_.joinable())
+      {
+        pimpl_->thread_.join();
+      }
+
+      pimpl_->workers_.reset(NULL);
+
+      /* drop the network, i.e. free memory of T_ASC_Network* structure. This call */
+      /* is the counterpart of ASC_initializeNetwork(...) which was called above. */
+      OFCondition cond = ASC_dropNetwork(&pimpl_->network_);
+      if (cond.bad())
+      {
+        LOG(ERROR) << "Error while dropping the network: " << cond.text();
+      }
+    }
+  }
+
+
+  bool DicomServer::IsMyAETitle(const std::string& aet) const
+  {
+    if (modalities_ == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    
+    if (!HasCalledApplicationEntityTitleCheck())
+    {
+      // OK, no check on the AET.
+      return true;
+    }
+    else
+    {
+      return modalities_->IsSameAETitle(aet, GetApplicationEntityTitle());
+    }
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomServer.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,136 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1
+#endif
+
+#include "IFindRequestHandlerFactory.h"
+#include "IMoveRequestHandlerFactory.h"
+#include "IStoreRequestHandlerFactory.h"
+#include "IWorklistRequestHandlerFactory.h"
+#include "IApplicationEntityFilter.h"
+#include "RemoteModalityParameters.h"
+
+#include <boost/shared_ptr.hpp>
+#include <boost/noncopyable.hpp>
+
+
+namespace Orthanc
+{
+  class DicomServer : public boost::noncopyable
+  {
+  public:
+    // WARNING: The methods of this class must be thread-safe
+    class IRemoteModalities : public boost::noncopyable
+    {
+    public:
+      virtual ~IRemoteModalities()
+      {
+      }
+      
+      virtual bool IsSameAETitle(const std::string& aet1,
+                                 const std::string& aet2) = 0;
+
+      virtual bool LookupAETitle(RemoteModalityParameters& modality,
+                                 const std::string& aet) = 0;
+    };
+    
+  private:
+    struct PImpl;
+    boost::shared_ptr<PImpl> pimpl_;
+
+    bool checkCalledAet_;
+    std::string aet_;
+    uint16_t port_;
+    bool continue_;
+    uint32_t associationTimeout_;
+    IRemoteModalities* modalities_;
+    IFindRequestHandlerFactory* findRequestHandlerFactory_;
+    IMoveRequestHandlerFactory* moveRequestHandlerFactory_;
+    IStoreRequestHandlerFactory* storeRequestHandlerFactory_;
+    IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_;
+    IApplicationEntityFilter* applicationEntityFilter_;
+
+    static void ServerThread(DicomServer* server);
+
+  public:
+    DicomServer();
+
+    ~DicomServer();
+
+    void SetPortNumber(uint16_t port);
+    uint16_t GetPortNumber() const;
+
+    void SetAssociationTimeout(uint32_t seconds);
+    uint32_t GetAssociationTimeout() const;
+
+    void SetCalledApplicationEntityTitleCheck(bool check);
+    bool HasCalledApplicationEntityTitleCheck() const;
+
+    void SetApplicationEntityTitle(const std::string& aet);
+    const std::string& GetApplicationEntityTitle() const;
+
+    void SetRemoteModalities(IRemoteModalities& modalities);
+    IRemoteModalities& GetRemoteModalities() const;
+    
+    void SetFindRequestHandlerFactory(IFindRequestHandlerFactory& handler);
+    bool HasFindRequestHandlerFactory() const;
+    IFindRequestHandlerFactory& GetFindRequestHandlerFactory() const;
+
+    void SetMoveRequestHandlerFactory(IMoveRequestHandlerFactory& handler);
+    bool HasMoveRequestHandlerFactory() const;
+    IMoveRequestHandlerFactory& GetMoveRequestHandlerFactory() const;
+
+    void SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& handler);
+    bool HasStoreRequestHandlerFactory() const;
+    IStoreRequestHandlerFactory& GetStoreRequestHandlerFactory() const;
+
+    void SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& handler);
+    bool HasWorklistRequestHandlerFactory() const;
+    IWorklistRequestHandlerFactory& GetWorklistRequestHandlerFactory() const;
+
+    void SetApplicationEntityFilter(IApplicationEntityFilter& handler);
+    bool HasApplicationEntityFilter() const;
+    IApplicationEntityFilter& GetApplicationEntityFilter() const;
+
+    void Start();
+  
+    void Stop();
+
+    bool IsMyAETitle(const std::string& aet) const;
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomUserConnection.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,1258 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+Copyright (C) 1994-2011, OFFIS e.V.
+All rights reserved.
+
+This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+- Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=========================================================================*/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomUserConnection.h"
+
+#include "../DicomFormat/DicomArray.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../DicomParsing/FromDcmtkBridge.h"
+#include "../DicomParsing/ToDcmtkBridge.h"
+
+#include <dcmtk/dcmdata/dcistrmb.h>
+#include <dcmtk/dcmdata/dcistrmf.h>
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcmetinf.h>
+#include <dcmtk/dcmnet/diutil.h>
+
+#include <set>
+
+
+#ifdef _WIN32
+/**
+ * "The maximum length, in bytes, of the string returned in the buffer 
+ * pointed to by the name parameter is dependent on the namespace provider,
+ * but this string must be 256 bytes or less.
+ * http://msdn.microsoft.com/en-us/library/windows/desktop/ms738527(v=vs.85).aspx
+ **/
+#  define HOST_NAME_MAX 256
+#  include <winsock.h>
+#endif 
+
+
+#if !defined(HOST_NAME_MAX) && defined(_POSIX_HOST_NAME_MAX)
+/**
+ * TO IMPROVE: "_POSIX_HOST_NAME_MAX is only the minimum value that
+ * HOST_NAME_MAX can ever have [...] Therefore you cannot allocate an
+ * array of size _POSIX_HOST_NAME_MAX, invoke gethostname() and expect
+ * that the result will fit."
+ * http://lists.gnu.org/archive/html/bug-gnulib/2009-08/msg00128.html
+ **/
+#define HOST_NAME_MAX _POSIX_HOST_NAME_MAX
+#endif
+
+
+static const char* DEFAULT_PREFERRED_TRANSFER_SYNTAX = UID_LittleEndianImplicitTransferSyntax;
+
+/**
+ * "If we have more than 64 storage SOP classes, tools such as
+ * storescu will fail because they attempt to negotiate two
+ * presentation contexts for each SOP class, and there is a total
+ * limit of 128 contexts for one association."
+ **/
+static const unsigned int MAXIMUM_STORAGE_SOP_CLASSES = 64;
+
+
+namespace Orthanc
+{
+  // By default, the timeout for DICOM SCU (client) connections is set to 10 seconds
+  static uint32_t defaultTimeout_ = 10;
+
+  struct DicomUserConnection::PImpl
+  {
+    // Connection state
+    uint32_t dimseTimeout_;
+    uint32_t acseTimeout_;
+    T_ASC_Network* net_;
+    T_ASC_Parameters* params_;
+    T_ASC_Association* assoc_;
+
+    bool IsOpen() const
+    {
+      return assoc_ != NULL;
+    }
+
+    void CheckIsOpen() const;
+
+    void Store(DcmInputStream& is, 
+               DicomUserConnection& connection,
+               const std::string& moveOriginatorAET,
+               uint16_t moveOriginatorID);
+  };
+
+
+  static void Check(const OFCondition& cond)
+  {
+    if (cond.bad())
+    {
+      LOG(ERROR) << "DicomUserConnection: " << std::string(cond.text());
+       throw OrthancException(ErrorCode_NetworkProtocol);
+    }
+  }
+
+  void DicomUserConnection::PImpl::CheckIsOpen() const
+  {
+    if (!IsOpen())
+    {
+      LOG(ERROR) << "DicomUserConnection: First open the connection";
+      throw OrthancException(ErrorCode_NetworkProtocol);
+    }
+  }
+
+
+  void DicomUserConnection::CheckIsOpen() const
+  {
+    pimpl_->CheckIsOpen();
+  }
+
+
+  static void RegisterStorageSOPClass(T_ASC_Parameters* params,
+                                      unsigned int& presentationContextId,
+                                      const std::string& sopClass,
+                                      const char* asPreferred[],
+                                      std::vector<const char*>& asFallback)
+  {
+    Check(ASC_addPresentationContext(params, presentationContextId, 
+                                     sopClass.c_str(), asPreferred, 1));
+    presentationContextId += 2;
+
+    if (asFallback.size() > 0)
+    {
+      Check(ASC_addPresentationContext(params, presentationContextId, 
+                                       sopClass.c_str(), &asFallback[0], asFallback.size()));
+      presentationContextId += 2;
+    }
+  }
+  
+    
+  void DicomUserConnection::SetupPresentationContexts(const std::string& preferredTransferSyntax)
+  {
+    // Flatten an array with the preferred transfer syntax
+    const char* asPreferred[1] = { preferredTransferSyntax.c_str() };
+
+    // Setup the fallback transfer syntaxes
+    std::set<std::string> fallbackSyntaxes;
+    fallbackSyntaxes.insert(UID_LittleEndianExplicitTransferSyntax);
+    fallbackSyntaxes.insert(UID_BigEndianExplicitTransferSyntax);
+    fallbackSyntaxes.insert(UID_LittleEndianImplicitTransferSyntax);
+    fallbackSyntaxes.erase(preferredTransferSyntax);
+
+    // Flatten an array with the fallback transfer syntaxes
+    std::vector<const char*> asFallback;
+    asFallback.reserve(fallbackSyntaxes.size());
+    for (std::set<std::string>::const_iterator 
+           it = fallbackSyntaxes.begin(); it != fallbackSyntaxes.end(); ++it)
+    {
+      asFallback.push_back(it->c_str());
+    }
+
+    CheckStorageSOPClassesInvariant();
+    unsigned int presentationContextId = 1;
+
+    for (std::list<std::string>::const_iterator it = reservedStorageSOPClasses_.begin();
+         it != reservedStorageSOPClasses_.end(); ++it)
+    {
+      RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
+                              *it, asPreferred, asFallback);
+    }
+
+    for (std::set<std::string>::const_iterator it = storageSOPClasses_.begin();
+         it != storageSOPClasses_.end(); ++it)
+    {
+      RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
+                              *it, asPreferred, asFallback);
+    }
+
+    for (std::set<std::string>::const_iterator it = defaultStorageSOPClasses_.begin();
+         it != defaultStorageSOPClasses_.end(); ++it)
+    {
+      RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
+                              *it, asPreferred, asFallback);
+    }
+  }
+
+
+  static bool IsGenericTransferSyntax(const std::string& syntax)
+  {
+    return (syntax == UID_LittleEndianExplicitTransferSyntax ||
+            syntax == UID_BigEndianExplicitTransferSyntax ||
+            syntax == UID_LittleEndianImplicitTransferSyntax);
+  }
+
+
+  void DicomUserConnection::PImpl::Store(DcmInputStream& is, 
+                                         DicomUserConnection& connection,
+                                         const std::string& moveOriginatorAET,
+                                         uint16_t moveOriginatorID)
+  {
+    DcmFileFormat dcmff;
+    Check(dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength));
+
+    // Determine the storage SOP class UID for this instance
+    static const DcmTagKey DCM_SOP_CLASS_UID(0x0008, 0x0016);
+    OFString sopClassUid;
+    if (dcmff.getDataset()->findAndGetOFString(DCM_SOP_CLASS_UID, sopClassUid).good())
+    {
+      connection.AddStorageSOPClass(sopClassUid.c_str());
+    }
+
+    // Determine whether a new presentation context must be
+    // negotiated, depending on the transfer syntax of this instance
+    DcmXfer xfer(dcmff.getDataset()->getOriginalXfer());
+    const std::string syntax(xfer.getXferID());
+    bool isGeneric = IsGenericTransferSyntax(syntax);
+
+    bool renegotiate;
+
+    if (!IsOpen())
+    {
+      renegotiate = true;
+    }
+    else if (isGeneric)
+    {
+      // Are we making a generic-to-specific or specific-to-generic change of
+      // the transfer syntax? If this is the case, renegotiate the connection.
+      renegotiate = !IsGenericTransferSyntax(connection.GetPreferredTransferSyntax());
+
+      if (renegotiate)
+      {
+        LOG(INFO) << "Use of non-generic transfer syntax: the C-Store associated must be renegotiated";
+      }
+    }
+    else
+    {
+      // We are using a specific transfer syntax. Renegotiate if the
+      // current connection does not match this transfer syntax.
+      renegotiate = (syntax != connection.GetPreferredTransferSyntax());
+
+      if (renegotiate)
+      {
+        LOG(INFO) << "Change in the transfer syntax: the C-Store associated must be renegotiated";
+      }
+    }
+
+    if (renegotiate)
+    {
+      if (isGeneric)
+      {
+        connection.ResetPreferredTransferSyntax();
+      }
+      else
+      {
+        connection.SetPreferredTransferSyntax(syntax);
+      }
+    }
+
+    if (!connection.IsOpen())
+    {
+      connection.Open();
+    }
+
+    // Figure out which SOP class and SOP instance is encapsulated in the file
+    DIC_UI sopClass;
+    DIC_UI sopInstance;
+    if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClass, sopInstance))
+    {
+      throw OrthancException(ErrorCode_NoSopClassOrInstance);
+    }
+
+    // Figure out which of the accepted presentation contexts should be used
+    int presID = ASC_findAcceptedPresentationContextID(assoc_, sopClass);
+    if (presID == 0)
+    {
+      const char *modalityName = dcmSOPClassUIDToModality(sopClass);
+      if (!modalityName) modalityName = dcmFindNameOfUID(sopClass);
+      if (!modalityName) modalityName = "unknown SOP class";
+      throw OrthancException(ErrorCode_NoPresentationContext);
+    }
+
+    // Prepare the transmission of data
+    T_DIMSE_C_StoreRQ request;
+    memset(&request, 0, sizeof(request));
+    request.MessageID = assoc_->nextMsgID++;
+    strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
+    request.Priority = DIMSE_PRIORITY_MEDIUM;
+    request.DataSetType = DIMSE_DATASET_PRESENT;
+    strncpy(request.AffectedSOPInstanceUID, sopInstance, DIC_UI_LEN);
+
+    if (!moveOriginatorAET.empty())
+    {
+      strncpy(request.MoveOriginatorApplicationEntityTitle, 
+              moveOriginatorAET.c_str(), DIC_AE_LEN);
+      request.opts = O_STORE_MOVEORIGINATORAETITLE;
+
+      request.MoveOriginatorID = moveOriginatorID;  // The type DIC_US is an alias for uint16_t
+      request.opts |= O_STORE_MOVEORIGINATORID;
+    }
+
+    // Finally conduct transmission of data
+    T_DIMSE_C_StoreRSP rsp;
+    DcmDataset* statusDetail = NULL;
+    Check(DIMSE_storeUser(assoc_, presID, &request,
+                          NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL,
+                          /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ dimseTimeout_,
+                          &rsp, &statusDetail, NULL));
+
+    if (statusDetail != NULL) 
+    {
+      delete statusDetail;
+    }
+  }
+
+
+  namespace
+  {
+    struct FindPayload
+    {
+      DicomFindAnswers* answers;
+      const char*       level;
+      bool              isWorklist;
+    };
+  }
+
+
+  static void FindCallback(
+    /* in */
+    void *callbackData,
+    T_DIMSE_C_FindRQ *request,      /* original find request */
+    int responseCount,
+    T_DIMSE_C_FindRSP *response,    /* pending response received */
+    DcmDataset *responseIdentifiers /* pending response identifiers */
+    )
+  {
+    FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData);
+
+    if (responseIdentifiers != NULL)
+    {
+      if (payload.isWorklist)
+      {
+        ParsedDicomFile answer(*responseIdentifiers);
+        payload.answers->Add(answer);
+      }
+      else
+      {
+        DicomMap m;
+        FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers);
+        
+        if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
+        {
+          m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false);
+        }
+
+        payload.answers->Add(m);
+      }
+    }
+  }
+
+
+  static void FixFindQuery(DicomMap& fixedQuery,
+                           ResourceType level,
+                           const DicomMap& fields)
+  {
+    std::set<DicomTag> allowedTags;
+
+    // WARNING: Do not add "break" or reorder items in this switch-case!
+    switch (level)
+    {
+      case ResourceType_Instance:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance);
+
+      case ResourceType_Series:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Series);
+
+      case ResourceType_Study:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Study);
+
+      case ResourceType_Patient:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
+        break;
+
+      case ResourceType_Study:
+        allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
+        allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY);
+        break;
+
+      case ResourceType_Series:
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
+        break;
+
+      default:
+        break;
+    }
+
+    allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET);
+
+    DicomArray query(fields);
+    for (size_t i = 0; i < query.GetSize(); i++)
+    {
+      const DicomTag& tag = query.GetElement(i).GetTag();
+      if (allowedTags.find(tag) == allowedTags.end())
+      {
+        LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag;
+      }
+      else
+      {
+        fixedQuery.SetValue(tag, query.GetElement(i).GetValue());
+      }
+    }
+  }
+
+
+  static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields,
+                                             ModalityManufacturer manufacturer)
+  {
+    // Fix outgoing C-Find requests issue for Syngo.Via and its
+    // solution was reported by Emsy Chan by private mail on
+    // 2015-06-17. According to Robert van Ommen (2015-11-30), the
+    // same fix is required for Agfa Impax. This was generalized for
+    // generic manufacturer since it seems to affect PhilipsADW,
+    // GEWAServer as well:
+    // https://bitbucket.org/sjodogne/orthanc/issues/31/
+
+    switch (manufacturer)
+    {
+      case ModalityManufacturer_GenericNoWildcardInDates:
+      case ModalityManufacturer_GenericNoUniversalWildcard:
+      {
+        std::auto_ptr<DicomMap> fix(fields.Clone());
+
+        std::set<DicomTag> tags;
+        fix->GetTags(tags);
+
+        for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+        {
+          // Replace a "*" wildcard query by an empty query ("") for
+          // "date" or "all" value representations depending on the
+          // type of manufacturer.
+          if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard ||
+              (manufacturer == ModalityManufacturer_GenericNoWildcardInDates &&
+               FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date))
+          {
+            const DicomValue* value = fix->TestAndGetValue(*it);
+
+            if (value != NULL && 
+                !value->IsNull() &&
+                value->GetContent() == "*")
+            {
+              fix->SetValue(*it, "", false);
+            }
+          }
+        }
+
+        return new ParsedDicomFile(*fix);
+      }
+
+      default:
+        return new ParsedDicomFile(fields);
+    }
+  }
+
+
+  static void ExecuteFind(DicomFindAnswers& answers,
+                          T_ASC_Association* association,
+                          DcmDataset* dataset,
+                          const char* sopClass,
+                          bool isWorklist,
+                          const char* level,
+                          uint32_t dimseTimeout)
+  {
+    assert(isWorklist ^ (level != NULL));
+
+    FindPayload payload;
+    payload.answers = &answers;
+    payload.level = level;
+    payload.isWorklist = isWorklist;
+
+    // Figure out which of the accepted presentation contexts should be used
+    int presID = ASC_findAcceptedPresentationContextID(association, sopClass);
+    if (presID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomFindUnavailable);
+    }
+
+    T_DIMSE_C_FindRQ request;
+    memset(&request, 0, sizeof(request));
+    request.MessageID = association->nextMsgID++;
+    strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
+    request.Priority = DIMSE_PRIORITY_MEDIUM;
+    request.DataSetType = DIMSE_DATASET_PRESENT;
+
+    T_DIMSE_C_FindRSP response;
+    DcmDataset* statusDetail = NULL;
+    OFCondition cond = DIMSE_findUser(association, presID, &request, dataset,
+                                      FindCallback, &payload,
+                                      /*opt_blockMode*/ DIMSE_BLOCKING, 
+                                      /*opt_dimse_timeout*/ dimseTimeout,
+                                      &response, &statusDetail);
+
+    if (statusDetail)
+    {
+      delete statusDetail;
+    }
+
+    Check(cond);
+  }
+
+
+  void DicomUserConnection::Find(DicomFindAnswers& result,
+                                 ResourceType level,
+                                 const DicomMap& originalFields)
+  {
+    DicomMap fields;
+    FixFindQuery(fields, level, originalFields);
+
+    CheckIsOpen();
+
+    std::auto_ptr<ParsedDicomFile> query(ConvertQueryFields(fields, manufacturer_));
+    DcmDataset* dataset = query->GetDcmtkObject().getDataset();
+
+    const char* clevel = NULL;
+    const char* sopClass = NULL;
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        clevel = "PATIENT";
+        DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "PATIENT");
+        sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
+        break;
+
+      case ResourceType_Study:
+        clevel = "STUDY";
+        DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "STUDY");
+        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
+        break;
+
+      case ResourceType_Series:
+        clevel = "SERIES";
+        DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "SERIES");
+        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
+        break;
+
+      case ResourceType_Instance:
+        clevel = "INSTANCE";
+        if (manufacturer_ == ModalityManufacturer_ClearCanvas ||
+            manufacturer_ == ModalityManufacturer_Dcm4Chee)
+        {
+          // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>.
+          // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J
+          // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx
+          DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "IMAGE");
+        }
+        else
+        {
+          DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "INSTANCE");
+        }
+
+        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    // Add the expected tags for this query level.
+    // WARNING: Do not reorder or add "break" in this switch-case!
+    switch (level)
+    {
+      case ResourceType_Instance:
+        // SOP Instance UID
+        if (!fields.HasTag(0x0008, 0x0018))
+          DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0018), "");
+
+      case ResourceType_Series:
+        // Series instance UID
+        if (!fields.HasTag(0x0020, 0x000e))
+          DU_putStringDOElement(dataset, DcmTagKey(0x0020, 0x000e), "");
+
+      case ResourceType_Study:
+        // Accession number
+        if (!fields.HasTag(0x0008, 0x0050))
+          DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0050), "");
+
+        // Study instance UID
+        if (!fields.HasTag(0x0020, 0x000d))
+          DU_putStringDOElement(dataset, DcmTagKey(0x0020, 0x000d), "");
+
+      case ResourceType_Patient:
+        // Patient ID
+        if (!fields.HasTag(0x0010, 0x0020))
+          DU_putStringDOElement(dataset, DcmTagKey(0x0010, 0x0020), "");
+
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(clevel != NULL && sopClass != NULL);
+    ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, false, clevel, pimpl_->dimseTimeout_);
+  }
+
+
+  void DicomUserConnection::MoveInternal(const std::string& targetAet,
+                                         ResourceType level,
+                                         const DicomMap& fields)
+  {
+    CheckIsOpen();
+
+    std::auto_ptr<ParsedDicomFile> query(ConvertQueryFields(fields, manufacturer_));
+    DcmDataset* dataset = query->GetDcmtkObject().getDataset();
+
+    const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
+    switch (level)
+    {
+      case ResourceType_Patient:
+        DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "PATIENT");
+        break;
+
+      case ResourceType_Study:
+        DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "STUDY");
+        break;
+
+      case ResourceType_Series:
+        DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "SERIES");
+        break;
+
+      case ResourceType_Instance:
+        if (manufacturer_ == ModalityManufacturer_ClearCanvas ||
+            manufacturer_ == ModalityManufacturer_Dcm4Chee)
+        {
+          // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>.
+          // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J
+          // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx
+          DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "IMAGE");
+        }
+        else
+        {
+          DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "INSTANCE");
+        }
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    // Figure out which of the accepted presentation contexts should be used
+    int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass);
+    if (presID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomMoveUnavailable);
+    }
+
+    T_DIMSE_C_MoveRQ request;
+    memset(&request, 0, sizeof(request));
+    request.MessageID = pimpl_->assoc_->nextMsgID++;
+    strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
+    request.Priority = DIMSE_PRIORITY_MEDIUM;
+    request.DataSetType = DIMSE_DATASET_PRESENT;
+    strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN);
+
+    T_DIMSE_C_MoveRSP response;
+    DcmDataset* statusDetail = NULL;
+    DcmDataset* responseIdentifiers = NULL;
+    OFCondition cond = DIMSE_moveUser(pimpl_->assoc_, presID, &request, dataset,
+                                      NULL, NULL,
+                                      /*opt_blockMode*/ DIMSE_BLOCKING, 
+                                      /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
+                                      pimpl_->net_, NULL, NULL,
+                                      &response, &statusDetail, &responseIdentifiers);
+
+    if (statusDetail)
+    {
+      delete statusDetail;
+    }
+
+    if (responseIdentifiers)
+    {
+      delete responseIdentifiers;
+    }
+
+    Check(cond);
+  }
+
+
+  void DicomUserConnection::ResetStorageSOPClasses()
+  {
+    CheckStorageSOPClassesInvariant();
+
+    storageSOPClasses_.clear();
+    defaultStorageSOPClasses_.clear();
+
+    // Copy the short list of storage SOP classes from DCMTK, making
+    // room for the 5 SOP classes reserved for C-ECHO, C-FIND, C-MOVE at (**).
+
+    std::set<std::string> uncommon;
+    uncommon.insert(UID_BlendingSoftcopyPresentationStateStorage);
+    uncommon.insert(UID_GrayscaleSoftcopyPresentationStateStorage);
+    uncommon.insert(UID_ColorSoftcopyPresentationStateStorage);
+    uncommon.insert(UID_PseudoColorSoftcopyPresentationStateStorage);
+    uncommon.insert(UID_XAXRFGrayscaleSoftcopyPresentationStateStorage);
+
+    // Add the storage syntaxes for C-STORE
+    for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs - 1; i++)
+    {
+      if (uncommon.find(dcmShortSCUStorageSOPClassUIDs[i]) == uncommon.end())
+      {
+        defaultStorageSOPClasses_.insert(dcmShortSCUStorageSOPClassUIDs[i]);
+      }
+    }
+
+    CheckStorageSOPClassesInvariant();
+  }
+
+
+  void DicomUserConnection::DefaultSetup()
+  {
+    preferredTransferSyntax_ = DEFAULT_PREFERRED_TRANSFER_SYNTAX;
+    localAet_ = "STORESCU";
+    remoteAet_ = "ANY-SCP";
+    remoteHost_ = "127.0.0.1";
+    remotePort_ = 104;
+    manufacturer_ = ModalityManufacturer_Generic;
+
+    SetTimeout(defaultTimeout_);
+    pimpl_->net_ = NULL;
+    pimpl_->params_ = NULL;
+    pimpl_->assoc_ = NULL;
+
+    // SOP classes for C-ECHO, C-FIND and C-MOVE (**)
+    reservedStorageSOPClasses_.push_back(UID_VerificationSOPClass);
+    reservedStorageSOPClasses_.push_back(UID_FINDPatientRootQueryRetrieveInformationModel);
+    reservedStorageSOPClasses_.push_back(UID_FINDStudyRootQueryRetrieveInformationModel);
+    reservedStorageSOPClasses_.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel);
+    reservedStorageSOPClasses_.push_back(UID_FINDModalityWorklistInformationModel);
+
+    ResetStorageSOPClasses();
+  }
+   
+
+  DicomUserConnection::DicomUserConnection() : 
+    pimpl_(new PImpl)
+  {
+    DefaultSetup();
+  }
+  
+
+  DicomUserConnection::DicomUserConnection(const std::string& localAet,
+                                           const RemoteModalityParameters& remote) : 
+    pimpl_(new PImpl)
+  {
+    DefaultSetup();
+    SetLocalApplicationEntityTitle(localAet);
+    SetRemoteModality(remote);
+  }
+
+
+  DicomUserConnection::~DicomUserConnection()
+  {
+    Close();
+  }
+
+
+  void DicomUserConnection::SetRemoteModality(const RemoteModalityParameters& parameters)
+  {
+    SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle());
+    SetRemoteHost(parameters.GetHost());
+    SetRemotePort(parameters.GetPortNumber());
+    SetRemoteManufacturer(parameters.GetManufacturer());
+  }
+
+
+  void DicomUserConnection::SetLocalApplicationEntityTitle(const std::string& aet)
+  {
+    if (localAet_ != aet)
+    {
+      Close();
+      localAet_ = aet;
+    }
+  }
+
+  void DicomUserConnection::SetRemoteApplicationEntityTitle(const std::string& aet)
+  {
+    if (remoteAet_ != aet)
+    {
+      Close();
+      remoteAet_ = aet;
+    }
+  }
+
+  void DicomUserConnection::SetRemoteManufacturer(ModalityManufacturer manufacturer)
+  {
+    if (manufacturer_ != manufacturer)
+    {
+      Close();
+      manufacturer_ = manufacturer;
+    }
+  }
+
+  void DicomUserConnection::ResetPreferredTransferSyntax()
+  {
+    SetPreferredTransferSyntax(DEFAULT_PREFERRED_TRANSFER_SYNTAX);
+  }
+
+  void DicomUserConnection::SetPreferredTransferSyntax(const std::string& preferredTransferSyntax)
+  {
+    if (preferredTransferSyntax_ != preferredTransferSyntax)
+    {
+      Close();
+      preferredTransferSyntax_ = preferredTransferSyntax;
+    }
+  }
+
+
+  void DicomUserConnection::SetRemoteHost(const std::string& host)
+  {
+    if (remoteHost_ != host)
+    {
+      if (host.size() > HOST_NAME_MAX - 10)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      Close();
+      remoteHost_ = host;
+    }
+  }
+
+  void DicomUserConnection::SetRemotePort(uint16_t port)
+  {
+    if (remotePort_ != port)
+    {
+      Close();
+      remotePort_ = port;
+    }
+  }
+
+  void DicomUserConnection::Open()
+  {
+    if (IsOpen())
+    {
+      // Don't reopen the connection
+      return;
+    }
+
+    LOG(INFO) << "Opening a DICOM SCU connection from AET \"" << GetLocalApplicationEntityTitle() 
+              << "\" to AET \"" << GetRemoteApplicationEntityTitle() << "\" on host "
+              << GetRemoteHost() << ":" << GetRemotePort() 
+              << " (manufacturer: " << EnumerationToString(GetRemoteManufacturer()) << ")";
+
+    Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_));
+    Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU));
+
+    // Set this application's title and the called application's title in the params
+    Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), remoteAet_.c_str(), NULL));
+
+    // Set the network addresses of the local and remote entities
+    char localHost[HOST_NAME_MAX];
+    gethostname(localHost, HOST_NAME_MAX - 1);
+
+    char remoteHostAndPort[HOST_NAME_MAX];
+
+#ifdef _MSC_VER
+    _snprintf
+#else
+      snprintf
+#endif
+      (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", remoteHost_.c_str(), remotePort_);
+
+    Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, remoteHostAndPort));
+
+    // Set various options
+    Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false));
+
+    SetupPresentationContexts(preferredTransferSyntax_);
+
+    // Do the association
+    Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_));
+
+    if (ASC_countAcceptedPresentationContexts(pimpl_->params_) == 0)
+    {
+      throw OrthancException(ErrorCode_NoPresentationContext);
+    }
+  }
+
+  void DicomUserConnection::Close()
+  {
+    if (pimpl_->assoc_ != NULL)
+    {
+      ASC_releaseAssociation(pimpl_->assoc_);
+      ASC_destroyAssociation(&pimpl_->assoc_);
+      pimpl_->assoc_ = NULL;
+      pimpl_->params_ = NULL;
+    }
+    else
+    {
+      if (pimpl_->params_ != NULL)
+      {
+        ASC_destroyAssociationParameters(&pimpl_->params_);
+        pimpl_->params_ = NULL;
+      }
+    }
+
+    if (pimpl_->net_ != NULL)
+    {
+      ASC_dropNetwork(&pimpl_->net_);
+      pimpl_->net_ = NULL;
+    }
+  }
+
+  bool DicomUserConnection::IsOpen() const
+  {
+    return pimpl_->IsOpen();
+  }
+
+  void DicomUserConnection::Store(const char* buffer, 
+                                  size_t size,
+                                  const std::string& moveOriginatorAET,
+                                  uint16_t moveOriginatorID)
+  {
+    // Prepare an input stream for the memory buffer
+    DcmInputBufferStream is;
+    if (size > 0)
+      is.setBuffer(buffer, size);
+    is.setEos();
+      
+    pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID);
+  }
+
+  void DicomUserConnection::Store(const std::string& buffer,
+                                  const std::string& moveOriginatorAET,
+                                  uint16_t moveOriginatorID)
+  {
+    if (buffer.size() > 0)
+      Store(reinterpret_cast<const char*>(&buffer[0]), buffer.size(), moveOriginatorAET, moveOriginatorID);
+    else
+      Store(NULL, 0, moveOriginatorAET, moveOriginatorID);
+  }
+
+  void DicomUserConnection::StoreFile(const std::string& path,
+                                      const std::string& moveOriginatorAET,
+                                      uint16_t moveOriginatorID)
+  {
+    // Prepare an input stream for the file
+    DcmInputFileStream is(path.c_str());
+    pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID);
+  }
+
+  bool DicomUserConnection::Echo()
+  {
+    CheckIsOpen();
+    DIC_US status;
+    Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++, 
+                         /*opt_blockMode*/ DIMSE_BLOCKING, 
+                         /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
+                         &status, NULL));
+    return status == STATUS_Success;
+  }
+
+
+  static void TestAndCopyTag(DicomMap& result,
+                             const DicomMap& source,
+                             const DicomTag& tag)
+  {
+    if (!source.HasTag(tag))
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+    else
+    {
+      result.SetValue(tag, source.GetValue(tag));
+    }
+  }
+
+
+  void DicomUserConnection::Move(const std::string& targetAet,
+                                 ResourceType level,
+                                 const DicomMap& findResult)
+  {
+    DicomMap move;
+    switch (level)
+    {
+      case ResourceType_Patient:
+        TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID);
+        break;
+
+      case ResourceType_Study:
+        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
+        break;
+
+      case ResourceType_Series:
+        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
+        TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
+        break;
+
+      case ResourceType_Instance:
+        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
+        TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
+        TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    MoveInternal(targetAet, level, move);
+  }
+
+
+  void DicomUserConnection::Move(const std::string& targetAet,
+                                 const DicomMap& findResult)
+  {
+    if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
+    ResourceType level = StringToResourceType(tmp.c_str());
+
+    Move(targetAet, level, findResult);
+  }
+
+
+  void DicomUserConnection::MovePatient(const std::string& targetAet,
+                                        const std::string& patientId)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false);
+    MoveInternal(targetAet, ResourceType_Patient, query);
+  }
+
+  void DicomUserConnection::MoveStudy(const std::string& targetAet,
+                                      const std::string& studyUid)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
+    MoveInternal(targetAet, ResourceType_Study, query);
+  }
+
+  void DicomUserConnection::MoveSeries(const std::string& targetAet,
+                                       const std::string& studyUid,
+                                       const std::string& seriesUid)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
+    query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
+    MoveInternal(targetAet, ResourceType_Series, query);
+  }
+
+  void DicomUserConnection::MoveInstance(const std::string& targetAet,
+                                         const std::string& studyUid,
+                                         const std::string& seriesUid,
+                                         const std::string& instanceUid)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
+    query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
+    query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false);
+    MoveInternal(targetAet, ResourceType_Instance, query);
+  }
+
+
+  void DicomUserConnection::SetTimeout(uint32_t seconds)
+  {
+    if (seconds == 0)
+    {
+      DisableTimeout();
+    }
+    else
+    {
+      dcmConnectionTimeout.set(seconds);
+      pimpl_->dimseTimeout_ = seconds;
+      pimpl_->acseTimeout_ = 10;  // Timeout used during association negociation
+    }
+  }
+
+
+  void DicomUserConnection::DisableTimeout()
+  {
+    /**
+     * Global timeout (seconds) for connecting to remote hosts.
+     * Default value is -1 which selects infinite timeout, i.e. blocking connect().
+     */
+    dcmConnectionTimeout.set(-1);
+    pimpl_->dimseTimeout_ = 0;
+    pimpl_->acseTimeout_ = 10;  // Timeout used during association negociation
+  }
+
+
+  void DicomUserConnection::CheckStorageSOPClassesInvariant() const
+  {
+    assert(storageSOPClasses_.size() + 
+           defaultStorageSOPClasses_.size() + 
+           reservedStorageSOPClasses_.size() <= MAXIMUM_STORAGE_SOP_CLASSES);
+  }
+
+  void DicomUserConnection::AddStorageSOPClass(const char* sop)
+  {
+    CheckStorageSOPClassesInvariant();
+
+    if (storageSOPClasses_.find(sop) != storageSOPClasses_.end())
+    {
+      // This storage SOP class is already explicitly registered. Do
+      // nothing.
+      return;
+    }
+
+    if (defaultStorageSOPClasses_.find(sop) != defaultStorageSOPClasses_.end())
+    {
+      // This storage SOP class is not explicitly registered, but is
+      // used by default. Just register it explicitly.
+      defaultStorageSOPClasses_.erase(sop);
+      storageSOPClasses_.insert(sop);
+
+      CheckStorageSOPClassesInvariant();
+      return;
+    }
+
+    // This storage SOP class is neither explicitly, nor implicitly
+    // registered. Close the connection and register it explicitly.
+
+    Close();
+
+    if (reservedStorageSOPClasses_.size() + 
+        storageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES)  // (*)
+    {
+      // The maximum number of SOP classes is reached
+      ResetStorageSOPClasses();
+      defaultStorageSOPClasses_.erase(sop);
+    }
+    else if (reservedStorageSOPClasses_.size() + storageSOPClasses_.size() + 
+             defaultStorageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES)
+    {
+      // Make room in the default storage syntaxes
+      assert(!defaultStorageSOPClasses_.empty());  // Necessarily true because condition (*) is false
+      defaultStorageSOPClasses_.erase(*defaultStorageSOPClasses_.rbegin());
+    }
+
+    // Explicitly register the new storage syntax
+    storageSOPClasses_.insert(sop);
+
+    CheckStorageSOPClassesInvariant();
+  }
+
+
+  void DicomUserConnection::FindWorklist(DicomFindAnswers& result,
+                                         ParsedDicomFile& query)
+  {
+    CheckIsOpen();
+
+    DcmDataset* dataset = query.GetDcmtkObject().getDataset();
+    const char* sopClass = UID_FINDModalityWorklistInformationModel;
+
+    ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, true, NULL, pimpl_->dimseTimeout_);
+  }
+
+  
+  void DicomUserConnection::SetDefaultTimeout(uint32_t seconds)
+  {
+    LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " 
+              << seconds << " seconds (0 = no timeout)";
+    defaultTimeout_ = seconds;
+  }  
+
+
+  bool DicomUserConnection::IsSameAssociation(const std::string& localAet,
+                                              const RemoteModalityParameters& remote) const
+  {
+    return (localAet_ == localAet &&
+            remoteAet_ == remote.GetApplicationEntityTitle() &&
+            remoteHost_ == remote.GetHost() &&
+            remotePort_ == remote.GetPortNumber() &&
+            manufacturer_ == remote.GetManufacturer());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomUserConnection.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,215 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1
+#endif
+
+#include "DicomFindAnswers.h"
+#include "../Enumerations.h"
+#include "RemoteModalityParameters.h"
+
+#include <stdint.h>
+#include <boost/shared_ptr.hpp>
+#include <boost/noncopyable.hpp>
+#include <list>
+
+namespace Orthanc
+{
+  class DicomUserConnection : public boost::noncopyable
+  {
+  private:
+    struct PImpl;
+    boost::shared_ptr<PImpl> pimpl_;
+
+    // Connection parameters
+    std::string preferredTransferSyntax_;
+    std::string localAet_;
+    std::string remoteAet_;
+    std::string remoteHost_;
+    uint16_t remotePort_;
+    ModalityManufacturer manufacturer_;
+    std::set<std::string> storageSOPClasses_;
+    std::list<std::string> reservedStorageSOPClasses_;
+    std::set<std::string> defaultStorageSOPClasses_;
+
+    void CheckIsOpen() const;
+
+    void SetupPresentationContexts(const std::string& preferredTransferSyntax);
+
+    void MoveInternal(const std::string& targetAet,
+                      ResourceType level,
+                      const DicomMap& fields);
+
+    void ResetStorageSOPClasses();
+
+    void CheckStorageSOPClassesInvariant() const;
+
+    void DefaultSetup();
+
+  public:
+    DicomUserConnection();
+
+    ~DicomUserConnection();
+
+    // This constructor corresponds to behavior of the old class
+    // "ReusableDicomUserConnection", without the call to "Open()"
+    DicomUserConnection(const std::string& localAet,
+                        const RemoteModalityParameters& remote);
+
+    void SetRemoteModality(const RemoteModalityParameters& parameters);
+
+    void SetLocalApplicationEntityTitle(const std::string& aet);
+
+    const std::string& GetLocalApplicationEntityTitle() const
+    {
+      return localAet_;
+    }
+
+    void SetRemoteApplicationEntityTitle(const std::string& aet);
+
+    const std::string& GetRemoteApplicationEntityTitle() const
+    {
+      return remoteAet_;
+    }
+
+    void SetRemoteHost(const std::string& host);
+
+    const std::string& GetRemoteHost() const
+    {
+      return remoteHost_;
+    }
+
+    void SetRemotePort(uint16_t port);
+
+    uint16_t GetRemotePort() const
+    {
+      return remotePort_;
+    }
+
+    void SetRemoteManufacturer(ModalityManufacturer manufacturer);
+
+    ModalityManufacturer GetRemoteManufacturer() const
+    {
+      return manufacturer_;
+    }
+
+    void ResetPreferredTransferSyntax();
+
+    void SetPreferredTransferSyntax(const std::string& preferredTransferSyntax);
+
+    const std::string& GetPreferredTransferSyntax() const
+    {
+      return preferredTransferSyntax_;
+    }
+
+    void AddStorageSOPClass(const char* sop);
+
+    void Open();
+
+    void Close();
+
+    bool IsOpen() const;
+
+    bool Echo();
+
+    void Store(const char* buffer, 
+               size_t size,
+               const std::string& moveOriginatorAET,
+               uint16_t moveOriginatorID);
+
+    void Store(const char* buffer, 
+               size_t size)
+    {
+      Store(buffer, size, "", 0);  // Not a C-Move
+    }
+
+    void Store(const std::string& buffer,
+               const std::string& moveOriginatorAET,
+               uint16_t moveOriginatorID);
+
+    void Store(const std::string& buffer)
+    {
+      Store(buffer, "", 0);  // Not a C-Move
+    }
+
+    void StoreFile(const std::string& path,
+                   const std::string& moveOriginatorAET,
+                   uint16_t moveOriginatorID);
+
+    void StoreFile(const std::string& path)
+    {
+      StoreFile(path, "", 0);  // Not a C-Move
+    }
+
+    void Find(DicomFindAnswers& result,
+              ResourceType level,
+              const DicomMap& fields);
+
+    void Move(const std::string& targetAet,
+              ResourceType level,
+              const DicomMap& findResult);
+
+    void Move(const std::string& targetAet,
+              const DicomMap& findResult);
+
+    void MovePatient(const std::string& targetAet,
+                     const std::string& patientId);
+
+    void MoveStudy(const std::string& targetAet,
+                   const std::string& studyUid);
+
+    void MoveSeries(const std::string& targetAet,
+                    const std::string& studyUid,
+                    const std::string& seriesUid);
+
+    void MoveInstance(const std::string& targetAet,
+                      const std::string& studyUid,
+                      const std::string& seriesUid,
+                      const std::string& instanceUid);
+
+    void SetTimeout(uint32_t seconds);
+
+    void DisableTimeout();
+
+    void FindWorklist(DicomFindAnswers& result,
+                      ParsedDicomFile& query);
+
+    static void SetDefaultTimeout(uint32_t seconds);
+
+    bool IsSameAssociation(const std::string& localAet,
+                           const RemoteModalityParameters& remote) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IApplicationEntityFilter.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,67 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Enumerations.h"
+
+#include <string>
+
+namespace Orthanc
+{
+  class IApplicationEntityFilter : public boost::noncopyable
+  {
+  public:
+    virtual ~IApplicationEntityFilter()
+    {
+    }
+
+    virtual bool IsAllowedConnection(const std::string& remoteIp,
+                                     const std::string& remoteAet,
+                                     const std::string& calledAet) = 0;
+
+    virtual bool IsAllowedRequest(const std::string& remoteIp,
+                                  const std::string& remoteAet,
+                                  const std::string& calledAet,
+                                  DicomRequestType type) = 0;
+
+    virtual bool IsAllowedTransferSyntax(const std::string& remoteIp,
+                                         const std::string& remoteAet,
+                                         const std::string& calledAet,
+                                         TransferSyntax syntax) = 0;
+
+    virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
+                                           const std::string& remoteAet,
+                                           const std::string& calledAet) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IDicomConnectionManager.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,82 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_DCMTK_NETWORKING)
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be defined
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING == 0
+
+namespace Orthanc
+{
+  // DICOM networking is disabled, this is just a void class
+  class IDicomConnectionManager : public boost::noncopyable
+  {
+  public:
+    virtual ~IDicomConnectionManager()
+    {
+    }
+  };
+}
+
+#else
+
+#include "DicomUserConnection.h"
+
+namespace Orthanc
+{
+  class IDicomConnectionManager : public boost::noncopyable
+  {
+  public:
+    virtual ~IDicomConnectionManager()
+    {
+    }
+
+    class IResource : public boost::noncopyable
+    {
+    public:
+      virtual ~IResource()
+      {
+      }
+
+      virtual DicomUserConnection& GetConnection() = 0;
+    };
+
+    virtual IResource* AcquireConnection(const std::string& localAet,
+                                         const RemoteModalityParameters& remote) = 0;
+  };
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IFindRequestHandler.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomFindAnswers.h"
+
+#include <list>
+
+namespace Orthanc
+{
+  class IFindRequestHandler : public boost::noncopyable
+  {
+  public:
+    virtual ~IFindRequestHandler()
+    {
+    }
+
+    virtual void Handle(DicomFindAnswers& answers,
+                        const DicomMap& input,
+                        const std::list<DicomTag>& sequencesToReturn,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet,
+                        ModalityManufacturer manufacturer) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IFindRequestHandlerFactory.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,49 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IFindRequestHandler.h"
+
+namespace Orthanc
+{
+  class IFindRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IFindRequestHandlerFactory()
+    {
+    }
+
+    virtual IFindRequestHandler* ConstructFindRequestHandler() = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IMoveRequestHandler.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,79 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/DicomFormat/DicomMap.h"
+
+#include <vector>
+#include <string>
+
+
+namespace Orthanc
+{
+  class IMoveRequestIterator : public boost::noncopyable
+  {
+  public:
+    enum Status
+    {
+      Status_Success,
+      Status_Failure,
+      Status_Warning
+    };
+
+    virtual ~IMoveRequestIterator()
+    {
+    }
+
+    virtual unsigned int GetSubOperationCount() const = 0;
+
+    virtual Status DoNext() = 0;
+  };
+
+
+  class IMoveRequestHandler
+  {
+  public:
+    virtual ~IMoveRequestHandler()
+    {
+    }
+
+    virtual IMoveRequestIterator* Handle(const std::string& targetAet,
+                                         const DicomMap& input,
+                                         const std::string& originatorIp,
+                                         const std::string& originatorAet,
+                                         const std::string& calledAet,
+                                         uint16_t originatorId) = 0;
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IMoveRequestHandlerFactory.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,49 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IMoveRequestHandler.h"
+
+namespace Orthanc
+{
+  class IMoveRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IMoveRequestHandlerFactory()
+    {
+    }
+
+    virtual IMoveRequestHandler* ConstructMoveRequestHandler() = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IStoreRequestHandler.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/DicomFormat/DicomMap.h"
+
+#include <vector>
+#include <string>
+#include <json/json.h>
+
+namespace Orthanc
+{
+  class IStoreRequestHandler : public boost::noncopyable
+  {
+  public:
+    virtual ~IStoreRequestHandler()
+    {
+    }
+
+    virtual void Handle(const std::string& dicomFile,
+                        const DicomMap& dicomSummary,
+                        const Json::Value& dicomJson,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IStoreRequestHandlerFactory.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,49 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IStoreRequestHandler.h"
+
+namespace Orthanc
+{
+  class IStoreRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IStoreRequestHandlerFactory()
+    {
+    }
+
+    virtual IStoreRequestHandler* ConstructStoreRequestHandler() = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IWorklistRequestHandler.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,54 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomFindAnswers.h"
+
+namespace Orthanc
+{
+  class IWorklistRequestHandler : public boost::noncopyable
+  {
+  public:
+    virtual ~IWorklistRequestHandler()
+    {
+    }
+
+    virtual void Handle(DicomFindAnswers& answers,
+                        ParsedDicomFile& query,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet,
+                        ModalityManufacturer manufacturer) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IWorklistRequestHandlerFactory.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,49 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IWorklistRequestHandler.h"
+
+namespace Orthanc
+{
+  class IWorklistRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IWorklistRequestHandlerFactory()
+    {
+    }
+
+    virtual IWorklistRequestHandler* ConstructWorklistRequestHandler() = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,934 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+Copyright (C) 1994-2011, OFFIS e.V.
+All rights reserved.
+
+This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+- Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=========================================================================*/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "CommandDispatcher.h"
+
+#include "FindScp.h"
+#include "StoreScp.h"
+#include "MoveScp.h"
+#include "../../Toolbox.h"
+#include "../../Logging.h"
+
+#include <dcmtk/dcmnet/dcasccfg.h>      /* for class DcmAssociationConfiguration */
+#include <boost/lexical_cast.hpp>
+
+static OFBool    opt_rejectWithoutImplementationUID = OFFalse;
+
+
+
+static DUL_PRESENTATIONCONTEXT *
+findPresentationContextID(LST_HEAD * head,
+                          T_ASC_PresentationContextID presentationContextID)
+{
+  DUL_PRESENTATIONCONTEXT *pc;
+  LST_HEAD **l;
+  OFBool found = OFFalse;
+
+  if (head == NULL)
+    return NULL;
+
+  l = &head;
+  if (*l == NULL)
+    return NULL;
+
+  pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Head(l));
+  (void)LST_Position(l, OFstatic_cast(LST_NODE *, pc));
+
+  while (pc && !found) {
+    if (pc->presentationContextID == presentationContextID) {
+      found = OFTrue;
+    } else {
+      pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Next(l));
+    }
+  }
+  return pc;
+}
+
+
+/** accept all presenstation contexts for unknown SOP classes,
+ *  i.e. UIDs appearing in the list of abstract syntaxes
+ *  where no corresponding name is defined in the UID dictionary.
+ *  @param params pointer to association parameters structure
+ *  @param transferSyntax transfer syntax to accept
+ *  @param acceptedRole SCU/SCP role to accept
+ */
+static OFCondition acceptUnknownContextsWithTransferSyntax(
+  T_ASC_Parameters * params,
+  const char* transferSyntax,
+  T_ASC_SC_ROLE acceptedRole)
+{
+  OFCondition cond = EC_Normal;
+  int n, i, k;
+  DUL_PRESENTATIONCONTEXT *dpc;
+  T_ASC_PresentationContext pc;
+  OFBool accepted = OFFalse;
+  OFBool abstractOK = OFFalse;
+
+  n = ASC_countPresentationContexts(params);
+  for (i = 0; i < n; i++)
+  {
+    cond = ASC_getPresentationContext(params, i, &pc);
+    if (cond.bad()) return cond;
+    abstractOK = OFFalse;
+    accepted = OFFalse;
+
+    if (dcmFindNameOfUID(pc.abstractSyntax) == NULL)
+    {
+      abstractOK = OFTrue;
+
+      /* check the transfer syntax */
+      for (k = 0; (k < OFstatic_cast(int, pc.transferSyntaxCount)) && !accepted; k++)
+      {
+        if (strcmp(pc.proposedTransferSyntaxes[k], transferSyntax) == 0)
+        {
+          accepted = OFTrue;
+        }
+      }
+    }
+
+    if (accepted)
+    {
+      cond = ASC_acceptPresentationContext(
+        params, pc.presentationContextID,
+        transferSyntax, acceptedRole);
+      if (cond.bad()) return cond;
+    } else {
+      T_ASC_P_ResultReason reason;
+
+      /* do not refuse if already accepted */
+      dpc = findPresentationContextID(params->DULparams.acceptedPresentationContext,
+                                      pc.presentationContextID);
+      if ((dpc == NULL) || ((dpc != NULL) && (dpc->result != ASC_P_ACCEPTANCE)))
+      {
+
+        if (abstractOK) {
+          reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED;
+        } else {
+          reason = ASC_P_ABSTRACTSYNTAXNOTSUPPORTED;
+        }
+        /*
+         * If previously this presentation context was refused
+         * because of bad transfer syntax let it stay that way.
+         */
+        if ((dpc != NULL) && (dpc->result == ASC_P_TRANSFERSYNTAXESNOTSUPPORTED))
+          reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED;
+
+        cond = ASC_refusePresentationContext(params, pc.presentationContextID, reason);
+        if (cond.bad()) return cond;
+      }
+    }
+  }
+  return EC_Normal;
+}
+
+
+/** accept all presenstation contexts for unknown SOP classes,
+ *  i.e. UIDs appearing in the list of abstract syntaxes
+ *  where no corresponding name is defined in the UID dictionary.
+ *  This method is passed a list of "preferred" transfer syntaxes.
+ *  @param params pointer to association parameters structure
+ *  @param transferSyntax transfer syntax to accept
+ *  @param acceptedRole SCU/SCP role to accept
+ */
+static OFCondition acceptUnknownContextsWithPreferredTransferSyntaxes(
+  T_ASC_Parameters * params,
+  const char* transferSyntaxes[], int transferSyntaxCount,
+  T_ASC_SC_ROLE acceptedRole = ASC_SC_ROLE_DEFAULT)
+{
+  OFCondition cond = EC_Normal;
+  /*
+  ** Accept in the order "least wanted" to "most wanted" transfer
+  ** syntax.  Accepting a transfer syntax will override previously
+  ** accepted transfer syntaxes.
+  */
+  for (int i = transferSyntaxCount - 1; i >= 0; i--)
+  {
+    cond = acceptUnknownContextsWithTransferSyntax(params, transferSyntaxes[i], acceptedRole);
+    if (cond.bad()) return cond;
+  }
+  return cond;
+}
+
+
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    /**
+     * EXTRACT OF FILE "dcmdata/libsrc/dcuid.cc" FROM DCMTK 3.6.0
+     * (dcmAllStorageSOPClassUIDs).
+     *
+     * an array of const strings containing all known Storage SOP
+     * Classes that fit into the conventional
+     * PATIENT-STUDY-SERIES-INSTANCE information model,
+     * i.e. everything a Storage SCP might want to store in a PACS.
+     * Special cases such as hanging protocol storage or the Storage
+     * SOP Class are not included in this list.
+     *
+     * THIS LIST CONTAINS ALL STORAGE SOP CLASSES INCLUDING RETIRED
+     * ONES AND IS LARGER THAN 64 ENTRIES.
+     */
+
+    const char* orthancStorageSOPClassUIDs[] =
+    {
+      UID_AmbulatoryECGWaveformStorage,
+      UID_ArterialPulseWaveformStorage,
+      UID_AutorefractionMeasurementsStorage,
+      UID_BasicStructuredDisplayStorage,
+      UID_BasicTextSRStorage,
+      UID_BasicVoiceAudioWaveformStorage,
+      UID_BlendingSoftcopyPresentationStateStorage,
+      UID_BreastTomosynthesisImageStorage,
+      UID_CardiacElectrophysiologyWaveformStorage,
+      UID_ChestCADSRStorage,
+      UID_ColonCADSRStorage,
+      UID_ColorSoftcopyPresentationStateStorage,
+      UID_ComprehensiveSRStorage,
+      UID_ComputedRadiographyImageStorage,
+      UID_CTImageStorage,
+      UID_DeformableSpatialRegistrationStorage,
+      UID_DigitalIntraOralXRayImageStorageForPresentation,
+      UID_DigitalIntraOralXRayImageStorageForProcessing,
+      UID_DigitalMammographyXRayImageStorageForPresentation,
+      UID_DigitalMammographyXRayImageStorageForProcessing,
+      UID_DigitalXRayImageStorageForPresentation,
+      UID_DigitalXRayImageStorageForProcessing,
+      UID_EncapsulatedCDAStorage,
+      UID_EncapsulatedPDFStorage,
+      UID_EnhancedCTImageStorage,
+      UID_EnhancedMRColorImageStorage,
+      UID_EnhancedMRImageStorage,
+      UID_EnhancedPETImageStorage,
+      UID_EnhancedSRStorage,
+      UID_EnhancedUSVolumeStorage,
+      UID_EnhancedXAImageStorage,
+      UID_EnhancedXRFImageStorage,
+      UID_GeneralAudioWaveformStorage,
+      UID_GeneralECGWaveformStorage,
+      UID_GenericImplantTemplateStorage,
+      UID_GrayscaleSoftcopyPresentationStateStorage,
+      UID_HemodynamicWaveformStorage,
+      UID_ImplantAssemblyTemplateStorage,
+      UID_ImplantationPlanSRDocumentStorage,
+      UID_ImplantTemplateGroupStorage,
+      UID_IntraocularLensCalculationsStorage,
+      UID_KeratometryMeasurementsStorage,
+      UID_KeyObjectSelectionDocumentStorage,
+      UID_LensometryMeasurementsStorage,
+      UID_MacularGridThicknessAndVolumeReportStorage,
+      UID_MammographyCADSRStorage,
+      UID_MRImageStorage,
+      UID_MRSpectroscopyStorage,
+      UID_MultiframeGrayscaleByteSecondaryCaptureImageStorage,
+      UID_MultiframeGrayscaleWordSecondaryCaptureImageStorage,
+      UID_MultiframeSingleBitSecondaryCaptureImageStorage,
+      UID_MultiframeTrueColorSecondaryCaptureImageStorage,
+      UID_NuclearMedicineImageStorage,
+      UID_OphthalmicAxialMeasurementsStorage,
+      UID_OphthalmicPhotography16BitImageStorage,
+      UID_OphthalmicPhotography8BitImageStorage,
+      UID_OphthalmicTomographyImageStorage,
+      UID_OphthalmicVisualFieldStaticPerimetryMeasurementsStorage,
+      UID_PositronEmissionTomographyImageStorage,
+      UID_ProcedureLogStorage,
+      UID_PseudoColorSoftcopyPresentationStateStorage,
+      UID_RawDataStorage,
+      UID_RealWorldValueMappingStorage,
+      UID_RespiratoryWaveformStorage,
+      UID_RTBeamsTreatmentRecordStorage,
+      UID_RTBrachyTreatmentRecordStorage,
+      UID_RTDoseStorage,
+      UID_RTImageStorage,
+      UID_RTIonBeamsTreatmentRecordStorage,
+      UID_RTIonPlanStorage,
+      UID_RTPlanStorage,
+      UID_RTStructureSetStorage,
+      UID_RTTreatmentSummaryRecordStorage,
+      UID_SecondaryCaptureImageStorage,
+      UID_SegmentationStorage,
+      UID_SpatialFiducialsStorage,
+      UID_SpatialRegistrationStorage,
+      UID_SpectaclePrescriptionReportStorage,
+      UID_StereometricRelationshipStorage,
+      UID_SubjectiveRefractionMeasurementsStorage,
+      UID_SurfaceSegmentationStorage,
+      UID_TwelveLeadECGWaveformStorage,
+      UID_UltrasoundImageStorage,
+      UID_UltrasoundMultiframeImageStorage,
+      UID_VideoEndoscopicImageStorage,
+      UID_VideoMicroscopicImageStorage,
+      UID_VideoPhotographicImageStorage,
+      UID_VisualAcuityMeasurementsStorage,
+      UID_VLEndoscopicImageStorage,
+      UID_VLMicroscopicImageStorage,
+      UID_VLPhotographicImageStorage,
+      UID_VLSlideCoordinatesMicroscopicImageStorage,
+      UID_VLWholeSlideMicroscopyImageStorage,
+      UID_XAXRFGrayscaleSoftcopyPresentationStateStorage,
+      UID_XRay3DAngiographicImageStorage,
+      UID_XRay3DCraniofacialImageStorage,
+      UID_XRayAngiographicImageStorage,
+      UID_XRayRadiationDoseSRStorage,
+      UID_XRayRadiofluoroscopicImageStorage,
+      // retired
+      UID_RETIRED_HardcopyColorImageStorage,
+      UID_RETIRED_HardcopyGrayscaleImageStorage,
+      UID_RETIRED_NuclearMedicineImageStorage,
+      UID_RETIRED_StandaloneCurveStorage,
+      UID_RETIRED_StandaloneModalityLUTStorage,
+      UID_RETIRED_StandaloneOverlayStorage,
+      UID_RETIRED_StandalonePETCurveStorage,
+      UID_RETIRED_StandaloneVOILUTStorage,
+      UID_RETIRED_StoredPrintStorage,
+      UID_RETIRED_UltrasoundImageStorage,
+      UID_RETIRED_UltrasoundMultiframeImageStorage,
+      UID_RETIRED_VLImageStorage,
+      UID_RETIRED_VLMultiFrameImageStorage,
+      UID_RETIRED_XRayAngiographicBiPlaneImageStorage,
+      // draft
+      UID_DRAFT_SRAudioStorage,
+      UID_DRAFT_SRComprehensiveStorage,
+      UID_DRAFT_SRDetailStorage,
+      UID_DRAFT_SRTextStorage,
+      UID_DRAFT_WaveformStorage,
+      UID_DRAFT_RTBeamsDeliveryInstructionStorage,
+      NULL
+    };
+
+    const int orthancStorageSOPClassUIDsCount = (sizeof(orthancStorageSOPClassUIDs) / sizeof(const char*)) - 1;
+
+
+
+    OFCondition AssociationCleanup(T_ASC_Association *assoc)
+    {
+      OFCondition cond = ASC_dropSCPAssociation(assoc);
+      if (cond.bad())
+      {
+        LOG(FATAL) << cond.text();
+        return cond;
+      }
+
+      cond = ASC_destroyAssociation(&assoc);
+      if (cond.bad())
+      {
+        LOG(FATAL) << cond.text();
+        return cond;
+      }
+
+      return cond;
+    }
+
+
+
+    CommandDispatcher* AcceptAssociation(const DicomServer& server, T_ASC_Network *net)
+    {
+      DcmAssociationConfiguration asccfg;
+      char buf[BUFSIZ];
+      T_ASC_Association *assoc;
+      OFCondition cond;
+      OFString sprofile;
+      OFString temp_str;
+
+      std::vector<const char*> knownAbstractSyntaxes;
+
+      // For C-STORE
+      if (server.HasStoreRequestHandlerFactory())
+      {
+        knownAbstractSyntaxes.push_back(UID_VerificationSOPClass);
+      }
+
+      // For C-FIND
+      if (server.HasFindRequestHandlerFactory())
+      {
+        knownAbstractSyntaxes.push_back(UID_FINDPatientRootQueryRetrieveInformationModel);
+        knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel);
+      }
+
+      if (server.HasWorklistRequestHandlerFactory())
+      {
+        knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel);
+      }
+
+      // For C-MOVE
+      if (server.HasMoveRequestHandlerFactory())
+      {
+        knownAbstractSyntaxes.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel);
+        knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel);
+      }
+
+      cond = ASC_receiveAssociation(net, &assoc, 
+                                    /*opt_maxPDU*/ ASC_DEFAULTMAXPDU, 
+                                    NULL, NULL,
+                                    /*opt_secureConnection*/ OFFalse,
+                                    DUL_NOBLOCK, 1);
+
+      if (cond == DUL_NOASSOCIATIONREQUEST)
+      {
+        // Timeout
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      // if some kind of error occured, take care of it
+      if (cond.bad())
+      {
+        LOG(ERROR) << "Receiving Association failed: " << cond.text();
+        // no matter what kind of error occurred, we need to do a cleanup
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      // Retrieve the AET and the IP address of the remote modality
+      std::string remoteAet;
+      std::string remoteIp;
+      std::string calledAet;
+  
+      {
+        DIC_AE remoteAet_C;
+        DIC_AE calledAet_C;
+        DIC_AE remoteIp_C;
+        DIC_AE calledIP_C;
+        if (ASC_getAPTitles(assoc->params, remoteAet_C, calledAet_C, NULL).bad() ||
+            ASC_getPresentationAddresses(assoc->params, remoteIp_C, calledIP_C).bad())
+        {
+          T_ASC_RejectParameters rej =
+            {
+              ASC_RESULT_REJECTEDPERMANENT,
+              ASC_SOURCE_SERVICEUSER,
+              ASC_REASON_SU_NOREASON
+            };
+          ASC_rejectAssociation(assoc, &rej);
+          AssociationCleanup(assoc);
+          return NULL;
+        }
+
+        remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C));
+        remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C));
+        calledAet = (/*OFSTRING_GUARD*/(calledAet_C));
+      }
+
+      LOG(INFO) << "Association Received from AET " << remoteAet 
+                << " on IP " << remoteIp;
+
+
+      std::vector<const char*> transferSyntaxes;
+
+      // This is the list of the transfer syntaxes that were supported up to Orthanc 0.7.1
+      transferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
+      transferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
+      transferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
+
+      // New transfer syntaxes supported since Orthanc 0.7.2
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated))
+      {
+        transferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); 
+      }
+
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg))
+      {
+        transferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax);
+      }
+
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000))
+      {
+        transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
+        transferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
+        transferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax);
+        transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax);
+      }
+
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless))
+      {
+        transferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax);
+      }
+
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip))
+      {
+        transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax);
+        transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax);
+      }
+
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2))
+      {
+        transferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax);
+        transferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax);
+      }
+
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle))
+      {
+        transferSyntaxes.push_back(UID_RLELosslessTransferSyntax);
+      }
+
+      /* accept the Verification SOP Class if presented */
+      cond = ASC_acceptContextsWithPreferredTransferSyntaxes(assoc->params, &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(), &transferSyntaxes[0], transferSyntaxes.size());
+      if (cond.bad())
+      {
+        LOG(INFO) << cond.text();
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      /* the array of Storage SOP Class UIDs comes from dcuid.h */
+      cond = ASC_acceptContextsWithPreferredTransferSyntaxes(assoc->params, orthancStorageSOPClassUIDs, orthancStorageSOPClassUIDsCount, &transferSyntaxes[0], transferSyntaxes.size());
+      if (cond.bad())
+      {
+        LOG(INFO) << cond.text();
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsUnknownSopClassAccepted(remoteIp, remoteAet, calledAet))
+      {
+        /*
+         * Promiscous mode is enabled: Accept everything not known not
+         * to be a storage SOP class.
+         **/
+        cond = acceptUnknownContextsWithPreferredTransferSyntaxes(
+          assoc->params, &transferSyntaxes[0], transferSyntaxes.size());
+        if (cond.bad())
+        {
+          LOG(INFO) << cond.text();
+          AssociationCleanup(assoc);
+          return NULL;
+        }
+      }
+
+      /* set our app title */
+      ASC_setAPTitles(assoc->params, NULL, NULL, server.GetApplicationEntityTitle().c_str());
+
+      /* acknowledge or reject this association */
+      cond = ASC_getApplicationContextName(assoc->params, buf);
+      if ((cond.bad()) || strcmp(buf, UID_StandardApplicationContext) != 0)
+      {
+        /* reject: the application context name is not supported */
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_APPCONTEXTNAMENOTSUPPORTED
+          };
+
+        LOG(INFO) << "Association Rejected: Bad Application Context Name: " << buf;
+        cond = ASC_rejectAssociation(assoc, &rej);
+        if (cond.bad())
+        {
+          LOG(INFO) << cond.text();
+        }
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      /* check the AETs */
+      if (!server.IsMyAETitle(calledAet))
+      {
+        LOG(WARNING) << "Rejected association, because of a bad called AET in the request (" << calledAet << ")";
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_CALLEDAETITLENOTRECOGNIZED
+          };
+        ASC_rejectAssociation(assoc, &rej);
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      if (server.HasApplicationEntityFilter() &&
+          !server.GetApplicationEntityFilter().IsAllowedConnection(remoteIp, remoteAet, calledAet))
+      {
+        LOG(WARNING) << "Rejected association for remote AET " << remoteAet << " on IP " << remoteIp;
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_CALLINGAETITLENOTRECOGNIZED
+          };
+        ASC_rejectAssociation(assoc, &rej);
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      if (opt_rejectWithoutImplementationUID && 
+          strlen(assoc->params->theirImplementationClassUID) == 0)
+      {
+        /* reject: the no implementation Class UID provided */
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_NOREASON
+          };
+
+        LOG(INFO) << "Association Rejected: No Implementation Class UID provided";
+        cond = ASC_rejectAssociation(assoc, &rej);
+        if (cond.bad())
+        {
+          LOG(INFO) << cond.text();
+        }
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      {
+        cond = ASC_acknowledgeAssociation(assoc);
+        if (cond.bad())
+        {
+          LOG(ERROR) << cond.text();
+          AssociationCleanup(assoc);
+          return NULL;
+        }
+        LOG(INFO) << "Association Acknowledged (Max Send PDV: " << assoc->sendPDVLength << ")";
+        if (ASC_countAcceptedPresentationContexts(assoc->params) == 0)
+          LOG(INFO) << "    (but no valid presentation contexts)";
+      }
+
+      IApplicationEntityFilter* filter = server.HasApplicationEntityFilter() ? &server.GetApplicationEntityFilter() : NULL;
+      return new CommandDispatcher(server, assoc, remoteIp, remoteAet, calledAet, filter);
+    }
+
+
+    CommandDispatcher::CommandDispatcher(const DicomServer& server,
+                                         T_ASC_Association* assoc,
+                                         const std::string& remoteIp,
+                                         const std::string& remoteAet,
+                                         const std::string& calledAet,
+                                         IApplicationEntityFilter* filter) :
+      server_(server),
+      assoc_(assoc),
+      remoteIp_(remoteIp),
+      remoteAet_(remoteAet),
+      calledAet_(calledAet),
+      filter_(filter)
+    {
+      associationTimeout_ = server.GetAssociationTimeout();
+      elapsedTimeSinceLastCommand_ = 0;
+    }
+
+
+    CommandDispatcher::~CommandDispatcher()
+    {
+      try
+      {
+        AssociationCleanup(assoc_);
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Some association was not cleanly aborted";
+      }
+    }
+
+
+    bool CommandDispatcher::Step()
+    /*
+     * This function receives DIMSE commmands over the network connection
+     * and handles these commands correspondingly. Note that in case of
+     * storscp only C-ECHO-RQ and C-STORE-RQ commands can be processed.
+     */
+    {
+      bool finished = false;
+
+      // receive a DIMSE command over the network, with a timeout of 1 second
+      DcmDataset *statusDetail = NULL;
+      T_ASC_PresentationContextID presID = 0;
+      T_DIMSE_Message msg;
+
+      OFCondition cond = DIMSE_receiveCommand(assoc_, DIMSE_NONBLOCKING, 1, &presID, &msg, &statusDetail);
+      elapsedTimeSinceLastCommand_++;
+    
+      // if the command which was received has extra status
+      // detail information, dump this information
+      if (statusDetail != NULL)
+      {
+        //LOG4CPP_WARN(Internals::GetLogger(), "Status Detail:" << OFendl << DcmObject::PrintHelper(*statusDetail));
+        delete statusDetail;
+      }
+
+      if (cond == DIMSE_OUTOFRESOURCES)
+      {
+        finished = true;
+      }
+      else if (cond == DIMSE_NODATAAVAILABLE)
+      {
+        // Timeout due to DIMSE_NONBLOCKING
+        if (associationTimeout_ != 0 && 
+            elapsedTimeSinceLastCommand_ >= associationTimeout_)
+        {
+          // This timeout is actually a association timeout
+          finished = true;
+        }
+      }
+      else if (cond == EC_Normal)
+      {
+        // Reset the association timeout counter
+        elapsedTimeSinceLastCommand_ = 0;
+
+        // Convert the type of request to Orthanc's internal type
+        bool supported = false;
+        DicomRequestType request;
+        switch (msg.CommandField)
+        {
+          case DIMSE_C_ECHO_RQ:
+            request = DicomRequestType_Echo;
+            supported = true;
+            break;
+
+          case DIMSE_C_STORE_RQ:
+            request = DicomRequestType_Store;
+            supported = true;
+            break;
+
+          case DIMSE_C_MOVE_RQ:
+            request = DicomRequestType_Move;
+            supported = true;
+            break;
+
+          case DIMSE_C_FIND_RQ:
+            request = DicomRequestType_Find;
+            supported = true;
+            break;
+
+          default:
+            // we cannot handle this kind of message
+            cond = DIMSE_BADCOMMANDTYPE;
+            LOG(ERROR) << "cannot handle command: 0x" << std::hex << msg.CommandField;
+            break;
+        }
+
+
+        // Check whether this request is allowed by the security filter
+        if (supported && 
+            filter_ != NULL &&
+            !filter_->IsAllowedRequest(remoteIp_, remoteAet_, calledAet_, request))
+        {
+          LOG(WARNING) << "Rejected " << EnumerationToString(request)
+                       << " request from remote DICOM modality with AET \""
+                       << remoteAet_ << "\" and hostname \"" << remoteIp_ << "\"";
+          cond = DIMSE_ILLEGALASSOCIATION;
+          supported = false;
+          finished = true;
+        }
+
+        // in case we received a supported message, process this command
+        if (supported)
+        {
+          // If anything goes wrong, there will be a "BADCOMMANDTYPE" answer
+          cond = DIMSE_BADCOMMANDTYPE;
+
+          switch (request)
+          {
+            case DicomRequestType_Echo:
+              cond = EchoScp(assoc_, &msg, presID);
+              break;
+
+            case DicomRequestType_Store:
+              if (server_.HasStoreRequestHandlerFactory()) // Should always be true
+              {
+                std::auto_ptr<IStoreRequestHandler> handler
+                  (server_.GetStoreRequestHandlerFactory().ConstructStoreRequestHandler());
+
+                if (handler.get() != NULL)
+                {
+                  cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_);
+                }
+              }
+              break;
+
+            case DicomRequestType_Move:
+              if (server_.HasMoveRequestHandlerFactory()) // Should always be true
+              {
+                std::auto_ptr<IMoveRequestHandler> handler
+                  (server_.GetMoveRequestHandlerFactory().ConstructMoveRequestHandler());
+
+                if (handler.get() != NULL)
+                {
+                  cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_);
+                }
+              }
+              break;
+
+            case DicomRequestType_Find:
+              if (server_.HasFindRequestHandlerFactory() || // Should always be true
+                  server_.HasWorklistRequestHandlerFactory())
+              {
+                std::auto_ptr<IFindRequestHandler> findHandler;
+                if (server_.HasFindRequestHandlerFactory())
+                {
+                  findHandler.reset(server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler());
+                }
+
+                std::auto_ptr<IWorklistRequestHandler> worklistHandler;
+                if (server_.HasWorklistRequestHandlerFactory())
+                {
+                  worklistHandler.reset(server_.GetWorklistRequestHandlerFactory().ConstructWorklistRequestHandler());
+                }
+
+                cond = Internals::findScp(assoc_, &msg, presID, server_.GetRemoteModalities(),
+                                          findHandler.get(), worklistHandler.get(),
+                                          remoteIp_, remoteAet_, calledAet_);
+              }
+              break;
+
+            default:
+              // Should never happen
+              break;
+          }
+        }
+      }
+      else
+      {
+        // Bad status, which indicates the closing of the connection by
+        // the peer or a network error
+        finished = true;
+
+        LOG(INFO) << cond.text();
+      }
+    
+      if (finished)
+      {
+        if (cond == DUL_PEERREQUESTEDRELEASE)
+        {
+          LOG(INFO) << "Association Release";
+          ASC_acknowledgeRelease(assoc_);
+        }
+        else if (cond == DUL_PEERABORTEDASSOCIATION)
+        {
+          LOG(INFO) << "Association Aborted";
+        }
+        else
+        {
+          OFString temp_str;
+          LOG(INFO) << "DIMSE failure (aborting association): " << cond.text();
+          /* some kind of error so abort the association */
+          ASC_abortAssociation(assoc_);
+        }
+      }
+
+      return !finished;
+    }
+
+
+    OFCondition EchoScp(T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID)
+    {
+      OFString temp_str;
+      LOG(INFO) << "Received Echo Request";
+      //LOG(DEBUG) << DIMSE_dumpMessage(temp_str, msg->msg.CEchoRQ, DIMSE_INCOMING, NULL, presID));
+
+      /* the echo succeeded !! */
+      OFCondition cond = DIMSE_sendEchoResponse(assoc, presID, &msg->msg.CEchoRQ, STATUS_Success, NULL);
+      if (cond.bad())
+      {
+        LOG(ERROR) << "Echo SCP Failed: " << cond.text();
+      }
+      return cond;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/Internals/CommandDispatcher.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,79 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../DicomServer.h"
+#include "../../MultiThreading/IRunnableBySteps.h"
+
+#include <dcmtk/dcmnet/dimse.h>
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    OFCondition AssociationCleanup(T_ASC_Association *assoc);
+
+    class CommandDispatcher : public IRunnableBySteps
+    {
+    private:
+      uint32_t associationTimeout_;
+      uint32_t elapsedTimeSinceLastCommand_;
+      const DicomServer& server_;
+      T_ASC_Association* assoc_;
+      std::string remoteIp_;
+      std::string remoteAet_;
+      std::string calledAet_;
+      IApplicationEntityFilter* filter_;
+
+    public:
+      CommandDispatcher(const DicomServer& server,
+                        T_ASC_Association* assoc,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet,
+                        IApplicationEntityFilter* filter);
+
+      virtual ~CommandDispatcher();
+
+      virtual bool Step();
+    };
+
+    OFCondition EchoScp(T_ASC_Association * assoc, 
+                        T_DIMSE_Message * msg, 
+                        T_ASC_PresentationContextID presID);
+
+    CommandDispatcher* AcceptAssociation(const DicomServer& server, 
+                                         T_ASC_Network *net);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/Internals/FindScp.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,348 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+Copyright (C) 1994-2011, OFFIS e.V.
+All rights reserved.
+
+This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+- Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=========================================================================*/
+
+
+
+#include "../../PrecompiledHeaders.h"
+#include "FindScp.h"
+
+#include "../../DicomParsing/FromDcmtkBridge.h"
+#include "../../DicomParsing/ToDcmtkBridge.h"
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+
+
+
+/**
+ * The function below is extracted from DCMTK 3.6.0, cf. file
+ * "dcmtk-3.6.0/dcmwlm/libsrc/wldsfs.cc".
+ **/
+
+static void HandleExistentButEmptyReferencedStudyOrPatientSequenceAttributes(DcmDataset *dataset, 
+                                                                             const DcmTagKey &sequenceTagKey)
+// Date         : May 3, 2005
+// Author       : Thomas Wilkens
+// Task         : This function performs a check on a sequence attribute in the given dataset. At two different places
+//                in the definition of the DICOM worklist management service, a sequence attribute with a return type
+//                of 2 is mentioned containing two 1C attributes in its item; the condition of the two 1C attributes
+//                specifies that in case a sequence item is present, then these two attributes must be existent and
+//                must contain a value. (I am talking about ReferencedStudySequence and ReferencedPatientSequence.)
+//                In cases where the sequence attribute contains exactly one item with an empty ReferencedSOPClass
+//                and an empty ReferencedSOPInstance, we want to remove the item from the sequence. This is what
+//                this function does.
+// Parameters   : dataset         - [in] Dataset in which the consistency of the sequence attribute shall be checked.
+//                sequenceTagKey  - [in] DcmTagKey of the sequence attribute which shall be checked.
+// Return Value : none.
+{
+  DcmElement *sequenceAttribute = NULL, *referencedSOPClassUIDAttribute = NULL, *referencedSOPInstanceUIDAttribute = NULL;
+
+  // in case the sequence attribute contains exactly one item with an empty
+  // ReferencedSOPClassUID and an empty ReferencedSOPInstanceUID, remove the item
+  if( dataset->findAndGetElement( sequenceTagKey, sequenceAttribute ).good() &&
+      ( (DcmSequenceOfItems*)sequenceAttribute )->card() == 1 &&
+      ( (DcmSequenceOfItems*)sequenceAttribute )->getItem(0)->findAndGetElement( DCM_ReferencedSOPClassUID, referencedSOPClassUIDAttribute ).good() &&
+      referencedSOPClassUIDAttribute->getLength() == 0 &&
+      ( (DcmSequenceOfItems*)sequenceAttribute )->getItem(0)->findAndGetElement( DCM_ReferencedSOPInstanceUID, referencedSOPInstanceUIDAttribute, OFFalse ).good() &&
+      referencedSOPInstanceUIDAttribute->getLength() == 0 )
+  {
+    DcmItem *item = ((DcmSequenceOfItems*)sequenceAttribute)->remove( ((DcmSequenceOfItems*)sequenceAttribute)->getItem(0) );
+    delete item;
+  }
+}
+
+
+
+namespace Orthanc
+{
+  namespace
+  {  
+    struct FindScpData
+    {
+      DicomServer::IRemoteModalities* modalities_;
+      IFindRequestHandler* findHandler_;
+      IWorklistRequestHandler* worklistHandler_;
+      DicomFindAnswers answers_;
+      DcmDataset* lastRequest_;
+      const std::string* remoteIp_;
+      const std::string* remoteAet_;
+      const std::string* calledAet_;
+
+      FindScpData() : answers_(false)
+      {
+      }
+    };
+
+
+
+    static void FixWorklistQuery(ParsedDicomFile& query)
+    {
+      // TODO: Check out
+      // WlmDataSourceFileSystem::HandleExistentButEmptyDescriptionAndCodeSequenceAttributes()"
+      // in DCMTK 3.6.0
+
+      DcmDataset* dataset = query.GetDcmtkObject().getDataset();      
+      HandleExistentButEmptyReferencedStudyOrPatientSequenceAttributes(dataset, DCM_ReferencedStudySequence);
+      HandleExistentButEmptyReferencedStudyOrPatientSequenceAttributes(dataset, DCM_ReferencedPatientSequence);
+    }
+
+
+
+    void FindScpCallback(
+      /* in */ 
+      void *callbackData,  
+      OFBool cancelled, 
+      T_DIMSE_C_FindRQ *request, 
+      DcmDataset *requestIdentifiers, 
+      int responseCount,
+      /* out */
+      T_DIMSE_C_FindRSP *response,
+      DcmDataset **responseIdentifiers,
+      DcmDataset **statusDetail)
+    {
+      bzero(response, sizeof(T_DIMSE_C_FindRSP));
+      *statusDetail = NULL;
+
+      std::string sopClassUid(request->AffectedSOPClassUID);
+
+      FindScpData& data = *reinterpret_cast<FindScpData*>(callbackData);
+      if (data.lastRequest_ == NULL)
+      {
+        bool ok = false;
+
+        try
+        {
+          RemoteModalityParameters modality;
+
+          /**
+           * Ensure that the remote modality is known to Orthanc for C-FIND requests.
+           **/
+
+          assert(data.modalities_ != NULL);
+          if (!data.modalities_->LookupAETitle(modality, *data.remoteAet_))
+          {
+            LOG(ERROR) << "Modality with AET \"" << *data.remoteAet_
+                       << "\" is not defined in the \"DicomModalities\" configuration option";
+            throw OrthancException(ErrorCode_UnknownModality);
+          }
+
+          
+          if (sopClassUid == UID_FINDModalityWorklistInformationModel)
+          {
+            data.answers_.SetWorklist(true);
+
+            if (data.worklistHandler_ != NULL)
+            {
+              ParsedDicomFile query(*requestIdentifiers);
+              FixWorklistQuery(query);
+
+              data.worklistHandler_->Handle(data.answers_, query,
+                                            *data.remoteIp_, *data.remoteAet_,
+                                            *data.calledAet_, modality.GetManufacturer());
+              ok = true;
+            }
+            else
+            {
+              LOG(ERROR) << "No worklist handler is installed, cannot handle this C-FIND request";
+            }
+          }
+          else
+          {
+            data.answers_.SetWorklist(false);
+
+            if (data.findHandler_ != NULL)
+            {
+              std::list<DicomTag> sequencesToReturn;
+
+              for (unsigned long i = 0; i < requestIdentifiers->card(); i++)
+              {
+                DcmElement* element = requestIdentifiers->getElement(i);
+                if (element && !element->isLeaf())
+                {
+                  const DicomTag tag(FromDcmtkBridge::Convert(element->getTag()));
+
+                  DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(*element);
+                  if (sequence.card() != 0)
+                  {
+                    LOG(WARNING) << "Orthanc only supports sequence matching on worklists, "
+                                 << "ignoring C-FIND SCU constraint on tag (" << tag.Format() 
+                                 << ") " << FromDcmtkBridge::GetTagName(*element);
+                  }
+
+                  sequencesToReturn.push_back(tag);
+                }
+              }
+
+              DicomMap input;
+              FromDcmtkBridge::ExtractDicomSummary(input, *requestIdentifiers);
+
+              data.findHandler_->Handle(data.answers_, input, sequencesToReturn,
+                                        *data.remoteIp_, *data.remoteAet_,
+                                        *data.calledAet_, modality.GetManufacturer());
+              ok = true;
+            }
+            else
+            {
+              LOG(ERROR) << "No C-Find handler is installed, cannot handle this request";
+            }
+          }
+        }
+        catch (OrthancException& e)
+        {
+          // Internal error!
+          LOG(ERROR) <<  "C-FIND request handler has failed: " << e.What();
+        }
+
+        if (!ok)
+        {
+          response->DimseStatus = STATUS_FIND_Failed_UnableToProcess;
+          *responseIdentifiers = NULL;   
+          return;
+        }
+
+        data.lastRequest_ = requestIdentifiers;
+      }
+      else if (data.lastRequest_ != requestIdentifiers)
+      {
+        // Internal error!
+        response->DimseStatus = STATUS_FIND_Failed_UnableToProcess;
+        *responseIdentifiers = NULL;   
+        return;
+      }
+
+      if (responseCount <= static_cast<int>(data.answers_.GetSize()))
+      {
+        // There are pending results that are still to be sent
+        response->DimseStatus = STATUS_Pending;
+        *responseIdentifiers = data.answers_.ExtractDcmDataset(responseCount - 1);
+      }
+      else if (data.answers_.IsComplete())
+      {
+        // Success: All the results have been sent
+        response->DimseStatus = STATUS_Success;
+        *responseIdentifiers = NULL;
+      }
+      else
+      {
+        // Success, but the results were too numerous and had to be cropped
+        LOG(WARNING) <<  "Too many results for an incoming C-FIND query";
+        response->DimseStatus = STATUS_FIND_Cancel_MatchingTerminatedDueToCancelRequest;
+        *responseIdentifiers = NULL;
+      }
+    }
+  }
+
+
+  OFCondition Internals::findScp(T_ASC_Association * assoc, 
+                                 T_DIMSE_Message * msg, 
+                                 T_ASC_PresentationContextID presID,
+                                 DicomServer::IRemoteModalities& modalities,
+                                 IFindRequestHandler* findHandler,
+                                 IWorklistRequestHandler* worklistHandler,
+                                 const std::string& remoteIp,
+                                 const std::string& remoteAet,
+                                 const std::string& calledAet)
+  {
+    FindScpData data;
+    data.modalities_ = &modalities;
+    data.findHandler_ = findHandler;
+    data.worklistHandler_ = worklistHandler;
+    data.lastRequest_ = NULL;
+    data.remoteIp_ = &remoteIp;
+    data.remoteAet_ = &remoteAet;
+    data.calledAet_ = &calledAet;
+
+    OFCondition cond = DIMSE_findProvider(assoc, presID, &msg->msg.CFindRQ, 
+                                          FindScpCallback, &data,
+                                          /*opt_blockMode*/ DIMSE_BLOCKING, 
+                                          /*opt_dimse_timeout*/ 0);
+
+    // if some error occured, dump corresponding information and remove the outfile if necessary
+    if (cond.bad())
+    {
+      OFString temp_str;
+      LOG(ERROR) << "Find SCP Failed: " << cond.text();
+    }
+
+    return cond;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/Internals/FindScp.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,54 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../DicomServer.h"
+
+#include <dcmtk/dcmnet/dimse.h>
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    OFCondition findScp(T_ASC_Association * assoc, 
+                        T_DIMSE_Message * msg, 
+                        T_ASC_PresentationContextID presID,
+                        DicomServer::IRemoteModalities& modalities,
+                        IFindRequestHandler* findHandler,   // can be NULL
+                        IWorklistRequestHandler* worklistHandler,   // can be NULL
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/Internals/MoveScp.cpp	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+Copyright (C) 1994-2011, OFFIS e.V.
+All rights reserved.
+
+This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+- Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=========================================================================*/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "MoveScp.h"
+
+#include <memory>
+
+#include "../../DicomParsing/FromDcmtkBridge.h"
+#include "../../DicomParsing/ToDcmtkBridge.h"
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+
+#include <boost/lexical_cast.hpp>
+
+
+/**
+ * Macro specifying whether to apply the patch suggested in issue 66:
+ * "Orthanc responses C-MOVE with zero Move Originator Message ID"
+ * https://bitbucket.org/sjodogne/orthanc/issues/66/
+ **/
+
+#define APPLY_FIX_ISSUE_66   1
+
+
+namespace Orthanc
+{
+  namespace
+  {  
+    struct MoveScpData
+    {
+      std::string target_;
+      IMoveRequestHandler* handler_;
+      DcmDataset* lastRequest_;
+      unsigned int subOperationCount_;
+      unsigned int failureCount_;
+      unsigned int warningCount_;
+      std::auto_ptr<IMoveRequestIterator> iterator_;
+      const std::string* remoteIp_;
+      const std::string* remoteAet_;
+      const std::string* calledAet_;
+    };
+
+
+#if APPLY_FIX_ISSUE_66 != 1
+    static uint16_t GetMessageId(const DicomMap& message)
+    {
+      /**
+       * Retrieve the Message ID (0000,0110) for this C-MOVE request, if
+       * any. If present, this Message ID will be stored in the Move
+       * Originator Message ID (0000,1031) field of the C-MOVE response.
+       * http://dicom.nema.org/dicom/2013/output/chtml/part07/chapter_E.html
+       **/
+
+      const DicomValue* value = message.TestAndGetValue(DICOM_TAG_MESSAGE_ID);
+
+      if (value != NULL &&
+          !value->IsNull() &&
+          !value->IsBinary())
+      {
+        try
+        {
+          int tmp = boost::lexical_cast<int>(value->GetContent());
+          if (tmp >= 0 && tmp <= 0xffff)
+          {
+            return static_cast<uint16_t>(tmp);
+          }
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+          LOG(WARNING) << "Cannot convert the Message ID (\"" << value->GetContent()
+                       << "\") of an incoming C-MOVE request to an integer, assuming zero";
+        }
+      }
+
+      return 0;
+    }
+#endif
+
+
+    void MoveScpCallback(
+      /* in */ 
+      void *callbackData,  
+      OFBool cancelled, 
+      T_DIMSE_C_MoveRQ *request, 
+      DcmDataset *requestIdentifiers, 
+      int responseCount,
+      /* out */
+      T_DIMSE_C_MoveRSP *response,
+      DcmDataset **responseIdentifiers,
+      DcmDataset **statusDetail)
+    {
+      bzero(response, sizeof(T_DIMSE_C_MoveRSP));
+      *statusDetail = NULL;
+      *responseIdentifiers = NULL;   
+
+      MoveScpData& data = *reinterpret_cast<MoveScpData*>(callbackData);
+      if (data.lastRequest_ == NULL)
+      {
+        DicomMap input;
+        FromDcmtkBridge::ExtractDicomSummary(input, *requestIdentifiers);
+
+        try
+        {
+#if APPLY_FIX_ISSUE_66 == 1
+          uint16_t messageId = request->MessageID;
+#else
+          // The line below was the implementation for Orthanc <= 1.3.2
+          uint16_t messageId = GetMessageId(input);
+#endif
+
+          data.iterator_.reset(data.handler_->Handle(data.target_, input, *data.remoteIp_, *data.remoteAet_,
+                                                     *data.calledAet_, messageId));
+
+          if (data.iterator_.get() == NULL)
+          {
+            // Internal error!
+            response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess;
+            return;
+          }
+
+          data.subOperationCount_ = data.iterator_->GetSubOperationCount();
+          data.failureCount_ = 0;
+          data.warningCount_ = 0;
+        }
+        catch (OrthancException& e)
+        {
+          // Internal error!
+          LOG(ERROR) << "IMoveRequestHandler Failed: " << e.What();
+          response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess;
+          return;
+        }
+
+        data.lastRequest_ = requestIdentifiers;
+      }
+      else if (data.lastRequest_ != requestIdentifiers)
+      {
+        // Internal error!
+        response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess;
+        return;
+      }
+  
+      if (data.subOperationCount_ == 0)
+      {
+        response->DimseStatus = STATUS_Success;
+      }
+      else
+      {
+        IMoveRequestIterator::Status status;
+
+        try
+        {
+          status = data.iterator_->DoNext();
+        }
+        catch (OrthancException& e)
+        {
+          // Internal error!
+          LOG(ERROR) << "IMoveRequestHandler Failed: " << e.What();
+          response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess;
+          return;
+        }
+
+        if (status == IMoveRequestIterator::Status_Failure)
+        {
+          data.failureCount_++;
+        }
+        else if (status == IMoveRequestIterator::Status_Warning)
+        {
+          data.warningCount_++;
+        }
+
+        if (responseCount < static_cast<int>(data.subOperationCount_))
+        {
+          response->DimseStatus = STATUS_Pending;
+        }
+        else
+        {
+          response->DimseStatus = STATUS_Success;
+        }
+      }
+
+      response->NumberOfRemainingSubOperations = data.subOperationCount_ - responseCount;
+      response->NumberOfCompletedSubOperations = responseCount;
+      response->NumberOfFailedSubOperations = data.failureCount_;
+      response->NumberOfWarningSubOperations = data.warningCount_;
+    }
+  }
+
+
+  OFCondition Internals::moveScp(T_ASC_Association * assoc, 
+                                 T_DIMSE_Message * msg, 
+                                 T_ASC_PresentationContextID presID,
+                                 IMoveRequestHandler& handler,
+                                 const std::string& remoteIp,
+                                 const std::string& remoteAet,
+                                 const std::string& calledAet)
+  {
+    MoveScpData data;
+    data.target_ = std::string(msg->msg.CMoveRQ.MoveDestination);
+    data.lastRequest_ = NULL;
+    data.handler_ = &handler;
+    data.remoteIp_ = &remoteIp;
+    data.remoteAet_ = &remoteAet;
+    data.calledAet_ = &calledAet;
+
+    OFCondition cond = DIMSE_moveProvider(assoc, presID, &msg->msg.CMoveRQ, 
+                                          MoveScpCallback, &data,
+                                          /*opt_blockMode*/ DIMSE_BLOCKING, 
+                                          /*opt_dimse_timeout*/ 0);
+
+    // if some error occured, dump corresponding information and remove the outfile if necessary
+    if (cond.bad())
+    {
+      OFString temp_str;
+      LOG(ERROR) << "Move SCP Failed: " << cond.text();
+    }
+
+    return cond;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/Internals/MoveScp.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../IMoveRequestHandler.h"
+
+#include <dcmtk/dcmnet/dimse.h>
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    OFCondition moveScp(T_ASC_Association * assoc, 
+                        T_DIMSE_Message * msg, 
+                        T_ASC_PresentationContextID presID,
+                        IMoveRequestHandler& handler,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/Internals/StoreScp.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,300 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+Copyright (C) 1994-2011, OFFIS e.V.
+All rights reserved.
+
+This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+- Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=========================================================================*/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "StoreScp.h"
+
+#include "../../DicomParsing/FromDcmtkBridge.h"
+#include "../../DicomParsing/ToDcmtkBridge.h"
+#include "../../OrthancException.h"
+#include "../../Logging.h"
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcmetinf.h>
+#include <dcmtk/dcmdata/dcostrmb.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmnet/diutil.h>
+
+
+namespace Orthanc
+{
+  namespace
+  {  
+    struct StoreCallbackData
+    {
+      IStoreRequestHandler* handler;
+      const std::string* remoteIp;
+      const char* remoteAET;
+      const char* calledAET;
+      const char* modality;
+      const char* affectedSOPInstanceUID;
+      uint32_t messageID;
+    };
+
+    
+    static void
+    storeScpCallback(
+      void *callbackData,
+      T_DIMSE_StoreProgress *progress,
+      T_DIMSE_C_StoreRQ *req,
+      char * /*imageFileName*/, DcmDataset **imageDataSet,
+      T_DIMSE_C_StoreRSP *rsp,
+      DcmDataset **statusDetail)
+    /*
+     * This function.is used to indicate progress when storescp receives instance data over the
+     * network. On the final call to this function (identified by progress->state == DIMSE_StoreEnd)
+     * this function will store the data set which was received over the network to a file.
+     * Earlier calls to this function will simply cause some information to be dumped to stdout.
+     *
+     * Parameters:
+     *   callbackData  - [in] data for this callback function
+     *   progress      - [in] The state of progress. (identifies if this is the initial or final call
+     *                   to this function, or a call in between these two calls.
+     *   req           - [in] The original store request message.
+     *   imageFileName - [in] The path to and name of the file the information shall be written to.
+     *   imageDataSet  - [in] The data set which shall be stored in the image file
+     *   rsp           - [inout] the C-STORE-RSP message (will be sent after the call to this function)
+     *   statusDetail  - [inout] This variable can be used to capture detailed information with regard to
+     *                   the status information which is captured in the status element (0000,0900). Note
+     *                   that this function does specify any such information, the pointer will be set to NULL.
+     */
+    {
+      StoreCallbackData *cbdata = OFstatic_cast(StoreCallbackData *, callbackData);
+
+      DIC_UI sopClass;
+      DIC_UI sopInstance;
+
+      // if this is the final call of this function, save the data which was received to a file
+      // (note that we could also save the image somewhere else, put it in database, etc.)
+      if (progress->state == DIMSE_StoreEnd)
+      {
+        OFString tmpStr;
+
+        // do not send status detail information
+        *statusDetail = NULL;
+
+        // Concerning the following line: an appropriate status code is already set in the resp structure,
+        // it need not be success. For example, if the caller has already detected an out of resources problem
+        // then the status will reflect this.  The callback function is still called to allow cleanup.
+        //rsp->DimseStatus = STATUS_Success;
+
+        // we want to write the received information to a file only if this information
+        // is present and the options opt_bitPreserving and opt_ignore are not set.
+        if ((imageDataSet != NULL) && (*imageDataSet != NULL))
+        {
+          DicomMap summary;
+          Json::Value dicomJson;
+          std::string buffer;
+
+          try
+          {
+            std::set<DicomTag> ignoreTagLength;
+            
+            FromDcmtkBridge::ExtractDicomSummary(summary, **imageDataSet);
+            FromDcmtkBridge::ExtractDicomAsJson(dicomJson, **imageDataSet, ignoreTagLength);
+
+            if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, **imageDataSet))
+            {
+              LOG(ERROR) << "cannot write DICOM file to memory";
+              rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources;
+            }
+          }
+          catch (...)
+          {
+            rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources;
+          }
+
+          // check the image to make sure it is consistent, i.e. that its sopClass and sopInstance correspond
+          // to those mentioned in the request. If not, set the status in the response message variable.
+          if (rsp->DimseStatus == STATUS_Success)
+          {
+            // which SOP class and SOP instance ?
+            if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sopInstance, /*opt_correctUIDPadding*/ OFFalse))
+            {
+              //LOG4CPP_ERROR(Internals::GetLogger(), "bad DICOM file: " << fileName);
+              rsp->DimseStatus = STATUS_STORE_Error_CannotUnderstand;
+            }
+            else if (strcmp(sopClass, req->AffectedSOPClassUID) != 0)
+            {
+              rsp->DimseStatus = STATUS_STORE_Error_DataSetDoesNotMatchSOPClass;
+            }
+            else if (strcmp(sopInstance, req->AffectedSOPInstanceUID) != 0)
+            {
+              rsp->DimseStatus = STATUS_STORE_Error_DataSetDoesNotMatchSOPClass;
+            }
+            else
+            {
+              try
+              {
+                cbdata->handler->Handle(buffer, summary, dicomJson, *cbdata->remoteIp, cbdata->remoteAET, cbdata->calledAET);
+              }
+              catch (OrthancException& e)
+              {
+                rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources;
+
+                if (e.GetErrorCode() == ErrorCode_InexistentTag)
+                {
+                  summary.LogMissingTagsForStore();
+                }
+                else
+                {
+                  LOG(ERROR) << "Exception while storing DICOM: " << e.What();
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+/*
+ * This function processes a DIMSE C-STORE-RQ commmand that was
+ * received over the network connection.
+ *
+ * Parameters:
+ *   assoc  - [in] The association (network connection to another DICOM application).
+ *   msg    - [in] The DIMSE C-STORE-RQ message that was received.
+ *   presID - [in] The ID of the presentation context which was specified in the PDV which contained
+ *                 the DIMSE command.
+ */
+  OFCondition Internals::storeScp(T_ASC_Association * assoc, 
+                                  T_DIMSE_Message * msg, 
+                                  T_ASC_PresentationContextID presID,
+                                  IStoreRequestHandler& handler,
+                                  const std::string& remoteIp)
+  {
+    OFCondition cond = EC_Normal;
+    T_DIMSE_C_StoreRQ *req;
+
+    // assign the actual information of the C-STORE-RQ command to a local variable
+    req = &msg->msg.CStoreRQ;
+
+    // intialize some variables
+    StoreCallbackData data;
+    data.handler = &handler;
+    data.remoteIp = &remoteIp;
+    data.modality = dcmSOPClassUIDToModality(req->AffectedSOPClassUID/*, "UNKNOWN"*/);
+    if (data.modality == NULL)
+      data.modality = "UNKNOWN";
+
+    data.affectedSOPInstanceUID = req->AffectedSOPInstanceUID;
+    data.messageID = req->MessageID;
+    if (assoc && assoc->params)
+    {
+      data.remoteAET = assoc->params->DULparams.callingAPTitle;
+      data.calledAET = assoc->params->DULparams.calledAPTitle;
+    }
+    else
+    {
+      data.remoteAET = "";
+      data.calledAET = "";
+    }
+
+    DcmFileFormat dcmff;
+
+    // store SourceApplicationEntityTitle in metaheader
+    if (assoc && assoc->params)
+    {
+      const char *aet = assoc->params->DULparams.callingAPTitle;
+      if (aet) dcmff.getMetaInfo()->putAndInsertString(DCM_SourceApplicationEntityTitle, aet);
+    }
+
+    // define an address where the information which will be received over the network will be stored
+    DcmDataset *dset = dcmff.getDataset();
+
+    cond = DIMSE_storeProvider(assoc, presID, req, NULL, /*opt_useMetaheader*/OFFalse, &dset,
+                               storeScpCallback, &data, 
+                               /*opt_blockMode*/ DIMSE_BLOCKING, 
+                               /*opt_dimse_timeout*/ 0);
+
+    // if some error occured, dump corresponding information and remove the outfile if necessary
+    if (cond.bad())
+    {
+      OFString temp_str;
+      LOG(ERROR) << "Store SCP Failed: " << cond.text();
+    }
+
+    // return return value
+    return cond;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/Internals/StoreScp.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../IStoreRequestHandler.h"
+
+#include <dcmtk/dcmnet/dimse.h>
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    OFCondition storeScp(T_ASC_Association * assoc, 
+                         T_DIMSE_Message * msg, 
+                         T_ASC_PresentationContextID presID,
+                         IStoreRequestHandler& handler,
+                         const std::string& remoteIp);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/RemoteModalityParameters.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,328 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "RemoteModalityParameters.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+
+#include <boost/lexical_cast.hpp>
+#include <stdexcept>
+
+
+static const char* KEY_AET = "AET";
+static const char* KEY_ALLOW_ECHO = "AllowEcho";
+static const char* KEY_ALLOW_FIND = "AllowFind";
+static const char* KEY_ALLOW_GET = "AllowGet";
+static const char* KEY_ALLOW_MOVE = "AllowMove";
+static const char* KEY_ALLOW_STORE = "AllowStore";
+static const char* KEY_HOST = "Host";
+static const char* KEY_MANUFACTURER = "Manufacturer";
+static const char* KEY_PORT = "Port";
+
+
+namespace Orthanc
+{
+  void RemoteModalityParameters::Clear()
+  {
+    aet_ = "ORTHANC";
+    host_ = "127.0.0.1";
+    port_ = 104;
+    manufacturer_ = ModalityManufacturer_Generic;
+    allowEcho_ = true;
+    allowStore_ = true;
+    allowFind_ = true;
+    allowMove_ = true;
+    allowGet_ = true;
+  }
+
+
+  RemoteModalityParameters::RemoteModalityParameters(const std::string& aet,
+                                                     const std::string& host,
+                                                     uint16_t port,
+                                                     ModalityManufacturer manufacturer)
+  {
+    Clear();
+    SetApplicationEntityTitle(aet);
+    SetHost(host);
+    SetPortNumber(port);
+    SetManufacturer(manufacturer);
+  }
+
+
+  static void CheckPortNumber(int value)
+  {
+    if (value <= 0 || 
+        value >= 65535)
+    {
+      LOG(ERROR) << "A TCP port number must be in range [1..65534], but found: " << value;
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static uint16_t ReadPortNumber(const Json::Value& value)
+  {
+    int tmp;
+
+    switch (value.type())
+    {
+      case Json::intValue:
+      case Json::uintValue:
+        tmp = value.asInt();
+        break;
+
+      case Json::stringValue:
+        try
+        {
+          tmp = boost::lexical_cast<int>(value.asString());
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    CheckPortNumber(tmp);
+    return static_cast<uint16_t>(tmp);
+  }
+
+
+  void RemoteModalityParameters::SetPortNumber(uint16_t port)
+  {
+    CheckPortNumber(port);
+    port_ = port;
+  }
+
+
+  void RemoteModalityParameters::UnserializeArray(const Json::Value& serialized)
+  {
+    assert(serialized.type() == Json::arrayValue);
+
+    if ((serialized.size() != 3 && 
+         serialized.size() != 4) ||
+        serialized[0].type() != Json::stringValue ||
+        serialized[1].type() != Json::stringValue ||
+        (serialized.size() == 4 &&
+         serialized[3].type() != Json::stringValue))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    aet_ = serialized[0].asString();
+    host_ = serialized[1].asString();
+    port_ = ReadPortNumber(serialized[2]);
+
+    if (serialized.size() == 4)
+    {
+      manufacturer_ = StringToModalityManufacturer(serialized[3].asString());
+    }
+    else
+    {
+      manufacturer_ = ModalityManufacturer_Generic;
+    }
+  }
+
+  
+  void RemoteModalityParameters::UnserializeObject(const Json::Value& serialized)
+  {
+    assert(serialized.type() == Json::objectValue);
+
+    aet_ = SerializationToolbox::ReadString(serialized, KEY_AET);
+    host_ = SerializationToolbox::ReadString(serialized, KEY_HOST);
+
+    if (serialized.isMember(KEY_PORT))
+    {
+      port_ = ReadPortNumber(serialized[KEY_PORT]);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    if (serialized.isMember(KEY_MANUFACTURER))
+    {
+      manufacturer_ = StringToModalityManufacturer
+        (SerializationToolbox::ReadString(serialized, KEY_MANUFACTURER));
+    }   
+    else
+    {
+      manufacturer_ = ModalityManufacturer_Generic;
+    }
+
+    if (serialized.isMember(KEY_ALLOW_ECHO))
+    {
+      allowEcho_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_ECHO);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_FIND))
+    {
+      allowFind_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_FIND);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_STORE))
+    {
+      allowStore_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_STORE);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_GET))
+    {
+      allowGet_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_GET);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_MOVE))
+    {
+      allowMove_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_MOVE);
+    }
+  }
+
+
+  bool RemoteModalityParameters::IsRequestAllowed(DicomRequestType type) const
+  {
+    switch (type)
+    {
+      case DicomRequestType_Echo:
+        return allowEcho_;
+
+      case DicomRequestType_Find:
+        return allowFind_;
+
+      case DicomRequestType_Get:
+        return allowGet_;
+
+      case DicomRequestType_Move:
+        return allowMove_;
+
+      case DicomRequestType_Store:
+        return allowStore_;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void RemoteModalityParameters::SetRequestAllowed(DicomRequestType type,
+                                                   bool allowed)
+  {
+    switch (type)
+    {
+      case DicomRequestType_Echo:
+        allowEcho_ = allowed;
+        break;
+
+      case DicomRequestType_Find:
+        allowFind_ = allowed;
+        break;
+
+      case DicomRequestType_Get:
+        allowGet_ = allowed;
+        break;
+
+      case DicomRequestType_Move:
+        allowMove_ = allowed;
+        break;
+
+      case DicomRequestType_Store:
+        allowStore_ = allowed;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool RemoteModalityParameters::IsAdvancedFormatNeeded() const
+  {
+    return (!allowEcho_ ||
+            !allowStore_ ||
+            !allowFind_ ||
+            !allowGet_ ||
+            !allowMove_);
+  }
+
+  
+  void RemoteModalityParameters::Serialize(Json::Value& target,
+                                           bool forceAdvancedFormat) const
+  {
+    if (forceAdvancedFormat ||
+        IsAdvancedFormatNeeded())
+    {
+      target = Json::objectValue;
+      target[KEY_AET] = aet_;
+      target[KEY_HOST] = host_;
+      target[KEY_PORT] = port_;
+      target[KEY_MANUFACTURER] = EnumerationToString(manufacturer_);
+      target[KEY_ALLOW_ECHO] = allowEcho_;
+      target[KEY_ALLOW_STORE] = allowStore_;
+      target[KEY_ALLOW_FIND] = allowFind_;
+      target[KEY_ALLOW_GET] = allowGet_;
+      target[KEY_ALLOW_MOVE] = allowMove_;
+    }
+    else
+    {
+      target = Json::arrayValue;
+      target.append(GetApplicationEntityTitle());
+      target.append(GetHost());
+      target.append(GetPortNumber());
+      target.append(EnumerationToString(GetManufacturer()));
+    }
+  }
+
+  
+  void RemoteModalityParameters::Unserialize(const Json::Value& serialized)
+  {
+    Clear();
+
+    switch (serialized.type())
+    {
+      case Json::objectValue:
+        UnserializeObject(serialized);
+        break;
+
+      case Json::arrayValue:
+        UnserializeArray(serialized);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/RemoteModalityParameters.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,133 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Enumerations.h"
+
+#include <stdint.h>
+#include <string>
+#include <json/json.h>
+
+namespace Orthanc
+{
+  class RemoteModalityParameters
+  {
+  private:
+    std::string           aet_;
+    std::string           host_;
+    uint16_t              port_;
+    ModalityManufacturer  manufacturer_;
+    bool                  allowEcho_;
+    bool                  allowStore_;
+    bool                  allowFind_;
+    bool                  allowMove_;
+    bool                  allowGet_;
+
+    void Clear();
+
+    void UnserializeArray(const Json::Value& serialized);
+
+    void UnserializeObject(const Json::Value& serialized);
+
+  public:
+    RemoteModalityParameters()
+    {
+      Clear();
+    }
+
+    RemoteModalityParameters(const Json::Value& serialized)
+    {
+      Unserialize(serialized);
+    }
+
+    RemoteModalityParameters(const std::string& aet,
+                             const std::string& host,
+                             uint16_t port,
+                             ModalityManufacturer manufacturer);
+
+    const std::string& GetApplicationEntityTitle() const
+    {
+      return aet_;
+    }
+
+    void SetApplicationEntityTitle(const std::string& aet)
+    {
+      aet_ = aet;
+    }
+
+    const std::string& GetHost() const
+    {
+      return host_;
+    }
+
+    void SetHost(const std::string& host)
+    {
+      host_ = host;
+    }
+    
+    uint16_t GetPortNumber() const
+    {
+      return port_;
+    }
+
+    void SetPortNumber(uint16_t port);
+
+    ModalityManufacturer GetManufacturer() const
+    {
+      return manufacturer_;
+    }
+
+    void SetManufacturer(ModalityManufacturer manufacturer)
+    {
+      manufacturer_ = manufacturer;
+    }    
+
+    void SetManufacturer(const std::string& manufacturer)
+    {
+      manufacturer_ = StringToModalityManufacturer(manufacturer);
+    }
+
+    bool IsRequestAllowed(DicomRequestType type) const;
+
+    void SetRequestAllowed(DicomRequestType type,
+                           bool allowed);
+
+    void Unserialize(const Json::Value& modality);
+
+    bool IsAdvancedFormatNeeded() const;
+
+    void Serialize(Json::Value& target,
+                   bool forceAdvancedFormat) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,134 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "TimeoutDicomConnectionManager.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  static boost::posix_time::ptime GetNow()
+  {
+    return boost::posix_time::microsec_clock::universal_time();
+  }
+
+  class TimeoutDicomConnectionManager::Resource : public IDicomConnectionManager::IResource
+  {
+  private:
+    TimeoutDicomConnectionManager&  that_;
+
+  public:
+    Resource(TimeoutDicomConnectionManager& that) : 
+      that_(that)
+    {
+      if (that_.connection_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    ~Resource()
+    {
+      that_.Touch();
+    }
+
+    DicomUserConnection& GetConnection()
+    {
+      assert(that_.connection_.get() != NULL);
+      return *that_.connection_;
+    }
+  };
+
+
+  void TimeoutDicomConnectionManager::Touch()
+  {
+    lastUse_ = GetNow();
+  }
+
+
+  void TimeoutDicomConnectionManager::CheckTimeoutInternal()
+  {
+    if (connection_.get() != NULL &&
+        (GetNow() - lastUse_) >= timeout_)
+    {
+      Close();
+    }
+  }
+
+
+  void TimeoutDicomConnectionManager::SetTimeout(unsigned int timeout)
+  {
+    timeout_ = boost::posix_time::milliseconds(timeout);
+    CheckTimeoutInternal();
+  }
+
+
+  unsigned int TimeoutDicomConnectionManager::GetTimeout()
+  {
+    return timeout_.total_milliseconds();
+  }
+
+
+  void TimeoutDicomConnectionManager::Close()
+  {
+    if (connection_.get() != NULL)
+    {
+      LOG(INFO) << "Closing inactive DICOM association with modality: "
+                << connection_->GetRemoteApplicationEntityTitle();
+
+      connection_.reset(NULL);
+    }
+  }
+
+
+  void TimeoutDicomConnectionManager::CheckTimeout()
+  {
+    CheckTimeoutInternal();
+  }
+
+
+  IDicomConnectionManager::IResource* 
+  TimeoutDicomConnectionManager::AcquireConnection(const std::string& localAet,
+                                                   const RemoteModalityParameters& remote)
+  {
+    if (connection_.get() == NULL ||
+        !connection_->IsSameAssociation(localAet, remote))
+    {
+      connection_.reset(new DicomUserConnection(localAet, remote));
+    }
+
+    return new Resource(*this);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,102 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IDicomConnectionManager.h"
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING == 0
+
+namespace Orthanc
+{
+  class TimeoutDicomConnectionManager : public IDicomConnectionManager
+  {
+  public:
+    void SetTimeout(unsigned int timeout)
+    {
+    }
+
+    unsigned int GetTimeout()
+    {
+      return 0;
+    }
+
+    void Close()
+    {
+    }
+
+    void CheckTimeout()
+    {
+    }
+  };
+}
+
+#else
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+namespace Orthanc
+{
+  class TimeoutDicomConnectionManager : public IDicomConnectionManager
+  {
+  private:
+    class Resource;
+
+    std::auto_ptr<DicomUserConnection>   connection_;
+    boost::posix_time::ptime             lastUse_;
+    boost::posix_time::time_duration     timeout_;
+
+    void Touch();
+
+    void CheckTimeoutInternal();
+
+  public:
+    TimeoutDicomConnectionManager() :
+      timeout_(boost::posix_time::milliseconds(1000))
+    {
+    }
+
+    void SetTimeout(unsigned int timeout);
+
+    unsigned int GetTimeout();
+
+    void Close();
+
+    void CheckTimeout();
+
+    virtual IResource* AcquireConnection(const std::string& localAet,
+                                         const RemoteModalityParameters& remote);
+  };
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/DicomDirWriter.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,582 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+Copyright (C) 1994-2011, OFFIS e.V.
+All rights reserved.
+
+This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+- Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=========================================================================*/
+
+
+
+/***
+    
+    Validation:
+
+    # sudo apt-get install dicom3tools
+    # dciodvfy DICOMDIR 2>&1 | less
+    # dcentvfy DICOMDIR 2>&1 | less
+
+    http://www.dclunie.com/dicom3tools/dciodvfy.html
+
+    DICOMDIR viewer working with Wine under Linux:
+    http://www.microdicom.com/
+
+ ***/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomDirWriter.h"
+
+#include "FromDcmtkBridge.h"
+#include "ToDcmtkBridge.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../TemporaryFile.h"
+#include "../Toolbox.h"
+#include "../SystemToolbox.h"
+
+#include <dcmtk/dcmdata/dcdicdir.h>
+#include <dcmtk/dcmdata/dcmetinf.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcuid.h>
+#include <dcmtk/dcmdata/dcddirif.h>
+#include <dcmtk/dcmdata/dcvrui.h>
+#include <dcmtk/dcmdata/dcsequen.h>
+#include <dcmtk/dcmdata/dcostrmf.h>
+#include "dcmtk/dcmdata/dcvrda.h"     /* for class DcmDate */
+#include "dcmtk/dcmdata/dcvrtm.h"     /* for class DcmTime */
+
+#include <memory>
+
+namespace Orthanc
+{
+  class DicomDirWriter::PImpl
+  {
+  private:
+    bool                       utc_;
+    std::string                fileSetId_;
+    bool                       extendedSopClass_;
+    TemporaryFile              file_;
+    std::auto_ptr<DcmDicomDir> dir_;
+
+    typedef std::pair<ResourceType, std::string>  IndexKey;
+    typedef std::map<IndexKey, DcmDirectoryRecord* >  Index;
+    Index  index_;
+
+
+    DcmDicomDir& GetDicomDir()
+    {
+      if (dir_.get() == NULL)
+      {
+        dir_.reset(new DcmDicomDir(file_.GetPath().c_str(), 
+                                   fileSetId_.c_str()));
+        //SetTagValue(dir_->getRootRecord(), DCM_SpecificCharacterSet, GetDicomSpecificCharacterSet(Encoding_Utf8));
+      }
+
+      return *dir_;
+    }
+
+
+    DcmDirectoryRecord& GetRoot()
+    {
+      return GetDicomDir().getRootRecord();
+    }
+
+
+    static bool GetUtf8TagValue(std::string& result,
+                                DcmItem& source,
+                                Encoding encoding,
+                                const DcmTagKey& key)
+    {
+      DcmElement* element = NULL;
+      result.clear();
+
+      if (source.findAndGetElement(key, element).good())
+      {
+        char* s = NULL;
+        if (element->isLeaf() &&
+            element->getString(s).good())
+        {
+          if (s != NULL)
+          {
+            result = Toolbox::ConvertToUtf8(s, encoding);
+          }
+          
+          return true;
+        }
+      }
+
+      return false;
+    }
+
+
+    static void SetTagValue(DcmDirectoryRecord& target,
+                            const DcmTagKey& key,
+                            const std::string& valueUtf8)
+    {
+      std::string s = Toolbox::ConvertFromUtf8(valueUtf8, Encoding_Ascii);
+
+      if (!target.putAndInsertString(key, s.c_str()).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+                            
+
+
+    static bool CopyString(DcmDirectoryRecord& target,
+                           DcmDataset& source,
+                           Encoding encoding,
+                           const DcmTagKey& key,
+                           bool optional,
+                           bool copyEmpty)
+    {
+      if (optional &&
+          !source.tagExistsWithValue(key) &&
+          !(copyEmpty && source.tagExists(key)))
+      {
+        return false;
+      }
+
+      std::string value;
+      bool found = GetUtf8TagValue(value, source, encoding, key);
+
+      if (!found)
+      {
+        // We don't raise an exception if "!optional", even if this
+        // results in an invalid DICOM file
+        value.clear();
+      }
+
+      SetTagValue(target, key, value);
+      return found;
+    }
+
+
+    static void CopyStringType1(DcmDirectoryRecord& target,
+                                DcmDataset& source,
+                                Encoding encoding,
+                                const DcmTagKey& key)
+    {
+      CopyString(target, source, encoding, key, false, false);
+    }
+
+    static void CopyStringType1C(DcmDirectoryRecord& target,
+                                 DcmDataset& source,
+                                 Encoding encoding,
+                                 const DcmTagKey& key)
+    {
+      CopyString(target, source, encoding, key, true, false);
+    }
+
+    static void CopyStringType2(DcmDirectoryRecord& target,
+                                DcmDataset& source,
+                                Encoding encoding,
+                                const DcmTagKey& key)
+    {
+      CopyString(target, source, encoding, key, false, true);
+    }
+
+    static void CopyStringType3(DcmDirectoryRecord& target,
+                                DcmDataset& source,
+                                Encoding encoding,
+                                const DcmTagKey& key)
+    {
+      CopyString(target, source, encoding, key, true, true);
+    }
+
+
+  public:
+    PImpl() :
+      utc_(true),   // By default, use UTC (universal time, not local time)
+      fileSetId_("ORTHANC_MEDIA"),
+      extendedSopClass_(false)
+    {
+    }
+    
+    bool IsUtcUsed() const
+    {
+      return utc_;
+    }
+
+
+    void SetUtcUsed(bool utc)
+    {
+      utc_ = utc;
+    }
+    
+    void EnableExtendedSopClass(bool enable)
+    {
+      if (enable)
+      {
+        LOG(WARNING) << "Generating a DICOMDIR with type 3 attributes, "
+                     << "which leads to an Extended SOP Class";
+      }
+      
+      extendedSopClass_ = enable;
+    }
+
+    bool IsExtendedSopClass() const
+    {
+      return extendedSopClass_;
+    }
+
+    void FillPatient(DcmDirectoryRecord& record,
+                     DcmDataset& dicom,
+                     Encoding encoding)
+    {
+      // cf. "DicomDirInterface::buildPatientRecord()"
+
+      CopyStringType1C(record, dicom, encoding, DCM_PatientID);
+      CopyStringType2(record, dicom, encoding, DCM_PatientName);
+    }
+
+    void FillStudy(DcmDirectoryRecord& record,
+                   DcmDataset& dicom,
+                   Encoding encoding)
+    {
+      // cf. "DicomDirInterface::buildStudyRecord()"
+
+      std::string nowDate, nowTime;
+      SystemToolbox::GetNowDicom(nowDate, nowTime, utc_);
+
+      std::string studyDate;
+      if (!GetUtf8TagValue(studyDate, dicom, encoding, DCM_StudyDate) &&
+          !GetUtf8TagValue(studyDate, dicom, encoding, DCM_SeriesDate) &&
+          !GetUtf8TagValue(studyDate, dicom, encoding, DCM_AcquisitionDate) &&
+          !GetUtf8TagValue(studyDate, dicom, encoding, DCM_ContentDate))
+      {
+        studyDate = nowDate;
+      }
+          
+      std::string studyTime;
+      if (!GetUtf8TagValue(studyTime, dicom, encoding, DCM_StudyTime) &&
+          !GetUtf8TagValue(studyTime, dicom, encoding, DCM_SeriesTime) &&
+          !GetUtf8TagValue(studyTime, dicom, encoding, DCM_AcquisitionTime) &&
+          !GetUtf8TagValue(studyTime, dicom, encoding, DCM_ContentTime))
+      {
+        studyTime = nowTime;
+      }
+
+      /* copy attribute values from dataset to study record */
+      SetTagValue(record, DCM_StudyDate, studyDate);
+      SetTagValue(record, DCM_StudyTime, studyTime);
+      CopyStringType2(record, dicom, encoding, DCM_StudyDescription);
+      CopyStringType1(record, dicom, encoding, DCM_StudyInstanceUID);
+      /* use type 1C instead of 1 in order to avoid unwanted overwriting */
+      CopyStringType1C(record, dicom, encoding, DCM_StudyID);
+      CopyStringType2(record, dicom, encoding, DCM_AccessionNumber);
+    }
+
+    void FillSeries(DcmDirectoryRecord& record,
+                    DcmDataset& dicom,
+                    Encoding encoding)
+    {
+      // cf. "DicomDirInterface::buildSeriesRecord()"
+
+      /* copy attribute values from dataset to series record */
+      CopyStringType1(record, dicom, encoding, DCM_Modality);
+      CopyStringType1(record, dicom, encoding, DCM_SeriesInstanceUID);
+      /* use type 1C instead of 1 in order to avoid unwanted overwriting */
+      CopyStringType1C(record, dicom, encoding, DCM_SeriesNumber);
+
+      // Add extended (non-standard) type 3 tags, those are not generated by DCMTK
+      // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part02/sect_7.3.html
+      // https://groups.google.com/d/msg/orthanc-users/Y7LOvZMDeoc/9cp3kDgxAwAJ
+      if (extendedSopClass_)
+      {
+        CopyStringType3(record, dicom, encoding, DCM_SeriesDescription);
+      }
+    }
+
+    void FillInstance(DcmDirectoryRecord& record,
+                      DcmDataset& dicom,
+                      Encoding encoding,
+                      DcmMetaInfo& metaInfo,
+                      const char* path)
+    {
+      // cf. "DicomDirInterface::buildImageRecord()"
+
+      /* copy attribute values from dataset to image record */
+      CopyStringType1(record, dicom, encoding, DCM_InstanceNumber);
+      //CopyElementType1C(record, dicom, encoding, DCM_ImageType);
+
+      // REMOVED since 0.9.7: copyElementType1C(dicom, DCM_ReferencedImageSequence, record);
+
+      std::string sopClassUid, sopInstanceUid, transferSyntaxUid;
+      if (!GetUtf8TagValue(sopClassUid, dicom, encoding, DCM_SOPClassUID) ||
+          !GetUtf8TagValue(sopInstanceUid, dicom, encoding, DCM_SOPInstanceUID) ||
+          !GetUtf8TagValue(transferSyntaxUid, metaInfo, encoding, DCM_TransferSyntaxUID))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      SetTagValue(record, DCM_ReferencedFileID, path);
+      SetTagValue(record, DCM_ReferencedSOPClassUIDInFile, sopClassUid);
+      SetTagValue(record, DCM_ReferencedSOPInstanceUIDInFile, sopInstanceUid);
+      SetTagValue(record, DCM_ReferencedTransferSyntaxUIDInFile, transferSyntaxUid);
+    }
+
+    
+
+    bool CreateResource(DcmDirectoryRecord*& target,
+                        ResourceType level,
+                        ParsedDicomFile& dicom,
+                        const char* filename,
+                        const char* path)
+    {
+      DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();
+      Encoding encoding = dicom.GetEncoding();
+
+      bool found;
+      std::string id;
+      E_DirRecType type;
+
+      switch (level)
+      {
+        case ResourceType_Patient:
+          found = GetUtf8TagValue(id, dataset, encoding, DCM_PatientID);
+          type = ERT_Patient;
+          break;
+
+        case ResourceType_Study:
+          found = GetUtf8TagValue(id, dataset, encoding, DCM_StudyInstanceUID);
+          type = ERT_Study;
+          break;
+
+        case ResourceType_Series:
+          found = GetUtf8TagValue(id, dataset, encoding, DCM_SeriesInstanceUID);
+          type = ERT_Series;
+          break;
+
+        case ResourceType_Instance:
+          found = GetUtf8TagValue(id, dataset, encoding, DCM_SOPInstanceUID);
+          type = ERT_Image;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (!found)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      IndexKey key = std::make_pair(level, std::string(id.c_str()));
+      Index::iterator it = index_.find(key);
+
+      if (it != index_.end())
+      {
+        target = it->second;
+        return false; // Already existing
+      }
+
+      std::auto_ptr<DcmDirectoryRecord> record(new DcmDirectoryRecord(type, NULL, filename));
+
+      switch (level)
+      {
+        case ResourceType_Patient:
+          FillPatient(*record, dataset, encoding);
+          break;
+
+        case ResourceType_Study:
+          FillStudy(*record, dataset, encoding);
+          break;
+
+        case ResourceType_Series:
+          FillSeries(*record, dataset, encoding);
+          break;
+
+        case ResourceType_Instance:
+          FillInstance(*record, dataset, encoding, *dicom.GetDcmtkObject().getMetaInfo(), path);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      CopyStringType1C(*record, dataset, encoding, DCM_SpecificCharacterSet);
+
+      target = record.get();
+      GetRoot().insertSub(record.release());
+      index_[key] = target;
+
+      return true;   // Newly created
+    }
+
+    void Read(std::string& s)
+    {
+      if (!GetDicomDir().write(DICOMDIR_DEFAULT_TRANSFERSYNTAX, 
+                               EET_UndefinedLength /*encodingType*/, 
+                               EGL_withoutGL /*groupLength*/).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      file_.Read(s);
+    }
+
+    void SetFileSetId(const std::string& id)
+    {
+      dir_.reset(NULL);
+      fileSetId_ = id;
+    }
+  };
+
+
+  DicomDirWriter::DicomDirWriter() : pimpl_(new PImpl)
+  {
+  }
+
+  void DicomDirWriter::SetUtcUsed(bool utc)
+  {
+    pimpl_->SetUtcUsed(utc);
+  }
+  
+  bool DicomDirWriter::IsUtcUsed() const
+  {
+    return pimpl_->IsUtcUsed();
+  }
+
+  void DicomDirWriter::SetFileSetId(const std::string& id)
+  {
+    pimpl_->SetFileSetId(id);
+  }
+
+  void DicomDirWriter::Add(const std::string& directory,
+                           const std::string& filename,
+                           ParsedDicomFile& dicom)
+  {
+    std::string path;
+    if (directory.empty())
+    {
+      path = filename;
+    }
+    else
+    {
+      if (directory[directory.length() - 1] == '/' ||
+          directory[directory.length() - 1] == '\\')
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      path = directory + '\\' + filename;
+    }
+
+    DcmDirectoryRecord* instance;
+    bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, dicom, filename.c_str(), path.c_str());
+    if (isNewInstance)
+    {
+      DcmDirectoryRecord* series;
+      bool isNewSeries = pimpl_->CreateResource(series, ResourceType_Series, dicom, filename.c_str(), NULL);
+      series->insertSub(instance);
+
+      if (isNewSeries)
+      {
+        DcmDirectoryRecord* study;
+        bool isNewStudy = pimpl_->CreateResource(study, ResourceType_Study, dicom, filename.c_str(), NULL);
+        study->insertSub(series);
+  
+        if (isNewStudy)
+        {
+          DcmDirectoryRecord* patient;
+          pimpl_->CreateResource(patient, ResourceType_Patient, dicom, filename.c_str(), NULL);
+          patient->insertSub(study);
+        }
+      }
+    }
+  }
+
+  void DicomDirWriter::Encode(std::string& target)
+  {
+    pimpl_->Read(target);
+  }
+
+
+  void DicomDirWriter::EnableExtendedSopClass(bool enable)
+  {
+    pimpl_->EnableExtendedSopClass(enable);
+  }
+
+  
+  bool DicomDirWriter::IsExtendedSopClass() const
+  {
+    return pimpl_->IsExtendedSopClass();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/DicomDirWriter.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ParsedDicomFile.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class DicomDirWriter : public boost::noncopyable
+  {
+  private:
+    class PImpl;
+    boost::shared_ptr<PImpl>  pimpl_;
+
+  public:
+    DicomDirWriter();
+
+    void SetUtcUsed(bool utc);
+
+    bool IsUtcUsed() const;
+
+    void SetFileSetId(const std::string& id);
+
+    void Add(const std::string& directory,
+             const std::string& filename,
+             ParsedDicomFile& dicom);
+
+    void Encode(std::string& target);
+
+    void EnableExtendedSopClass(bool enable);
+
+    bool IsExtendedSopClass() const;
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/DicomModification.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,1415 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomModification.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+#include "FromDcmtkBridge.h"
+#include "ITagVisitor.h"
+
+#include <memory>   // For std::auto_ptr
+
+
+static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2008 =
+  "Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1";
+
+static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2017c =
+  "Orthanc " ORTHANC_VERSION " - PS 3.15-2017c Table E.1-1 Basic Profile";
+
+namespace Orthanc
+{
+  class DicomModification::RelationshipsVisitor : public ITagVisitor
+  {
+  private:
+    DicomModification&  that_;
+    
+    bool IsEnabled(const DicomTag& tag) const
+    {
+      return (!that_.IsCleared(tag) &&
+              !that_.IsRemoved(tag) &&
+              !that_.IsReplaced(tag));
+    }
+
+    void RemoveIfEnabled(ParsedDicomFile& dicom,
+                         const DicomTag& tag) const
+    {
+      if (IsEnabled(tag))
+      {
+        dicom.Remove(tag);
+      }
+    }
+                         
+
+  public:
+    RelationshipsVisitor(DicomModification&  that) :
+    that_(that)
+    {
+    }
+
+    virtual void VisitUnknown(const std::vector<DicomTag>& parentTags,
+                              const std::vector<size_t>& parentIndexes,
+                              const DicomTag& tag,
+                              ValueRepresentation vr)
+    {
+    }
+
+    virtual void VisitBinary(const std::vector<DicomTag>& parentTags,
+                             const std::vector<size_t>& parentIndexes,
+                             const DicomTag& tag,
+                             ValueRepresentation vr,
+                             const void* data,
+                             size_t size)
+    {
+    }
+
+    virtual void VisitInteger(const std::vector<DicomTag>& parentTags,
+                              const std::vector<size_t>& parentIndexes,
+                              const DicomTag& tag,
+                              ValueRepresentation vr,
+                              int64_t value)
+    {
+    }
+
+    virtual void VisitDouble(const std::vector<DicomTag>& parentTags,
+                             const std::vector<size_t>& parentIndexes,
+                             const DicomTag& tag,
+                             ValueRepresentation vr,
+                             double value)
+    {
+    }
+
+    virtual void VisitAttribute(const std::vector<DicomTag>& parentTags,
+                                const std::vector<size_t>& parentIndexes,
+                                const DicomTag& tag,
+                                ValueRepresentation vr,
+                                const DicomTag& value)
+    {
+    }
+
+    virtual Action VisitString(std::string& newValue,
+                               const std::vector<DicomTag>& parentTags,
+                               const std::vector<size_t>& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const std::string& value)
+    {
+      if (!IsEnabled(tag))
+      {
+        return Action_None;
+      }
+      else if (tag == DICOM_TAG_FRAME_OF_REFERENCE_UID || 
+               tag == DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_UID || 
+               tag == DICOM_TAG_REFERENCED_SOP_INSTANCE_UID ||
+               tag == DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID)
+      {
+        newValue = that_.MapDicomIdentifier(Toolbox::StripSpaces(value), ResourceType_Instance);
+        return Action_Replace;
+      }
+      else if (parentTags.size() == 1 &&
+               parentTags[0] == DICOM_TAG_CURRENT_REQUESTED_PROCEDURE_EVIDENCE_SEQUENCE &&
+               tag == DICOM_TAG_STUDY_INSTANCE_UID)
+      {
+        newValue = that_.MapDicomIdentifier(Toolbox::StripSpaces(value), ResourceType_Study);
+        return Action_Replace;
+      }
+      else if (parentTags.size() == 2 &&
+               parentTags[0] == DICOM_TAG_CURRENT_REQUESTED_PROCEDURE_EVIDENCE_SEQUENCE &&
+               parentTags[1] == DICOM_TAG_REFERENCED_SERIES_SEQUENCE &&
+               tag == DICOM_TAG_SERIES_INSTANCE_UID)
+      {
+        newValue = that_.MapDicomIdentifier(Toolbox::StripSpaces(value), ResourceType_Series);
+        return Action_Replace;
+      }
+      else
+      {
+        return Action_None;
+      }
+    }
+
+    void RemoveRelationships(ParsedDicomFile& dicom) const
+    {
+      // Sequences containing the UID relationships
+      RemoveIfEnabled(dicom, DICOM_TAG_REFERENCED_IMAGE_SEQUENCE);
+      RemoveIfEnabled(dicom, DICOM_TAG_SOURCE_IMAGE_SEQUENCE);
+      
+      // Individual tags
+      RemoveIfEnabled(dicom, DICOM_TAG_FRAME_OF_REFERENCE_UID);
+
+      // The tags below should never occur at the first level of the
+      // hierarchy, but remove them anyway
+      RemoveIfEnabled(dicom, DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_UID);
+      RemoveIfEnabled(dicom, DICOM_TAG_REFERENCED_SOP_INSTANCE_UID);
+      RemoveIfEnabled(dicom, DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID);
+    }
+  };
+
+
+  bool DicomModification::CancelReplacement(const DicomTag& tag)
+  {
+    Replacements::iterator it = replacements_.find(tag);
+    
+    if (it != replacements_.end())
+    {
+      delete it->second;
+      replacements_.erase(it);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  void DicomModification::ReplaceInternal(const DicomTag& tag,
+                                          const Json::Value& value)
+  {
+    Replacements::iterator it = replacements_.find(tag);
+
+    if (it != replacements_.end())
+    {
+      delete it->second;
+      it->second = NULL;   // In the case of an exception during the clone
+      it->second = new Json::Value(value);  // Clone
+    }
+    else
+    {
+      replacements_[tag] = new Json::Value(value);  // Clone
+    }
+  }
+
+
+  void DicomModification::ClearReplacements()
+  {
+    for (Replacements::iterator it = replacements_.begin();
+         it != replacements_.end(); ++it)
+    {
+      delete it->second;
+    }
+
+    replacements_.clear();
+  }
+
+
+  void DicomModification::MarkNotOrthancAnonymization()
+  {
+    Replacements::iterator it = replacements_.find(DICOM_TAG_DEIDENTIFICATION_METHOD);
+
+    if (it != replacements_.end() &&
+        (it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2008 ||
+         it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2017c))
+    {
+      delete it->second;
+      replacements_.erase(it);
+    }
+  }
+
+
+
+  std::string DicomModification::MapDicomIdentifier(const std::string& original,
+                                                    ResourceType level)
+  {
+    std::string mapped;
+
+    UidMap::const_iterator previous = uidMap_.find(std::make_pair(level, original));
+
+    if (previous == uidMap_.end())
+    {
+      if (identifierGenerator_ == NULL)
+      {
+        mapped = FromDcmtkBridge::GenerateUniqueIdentifier(level);
+      }
+      else
+      {
+        if (!identifierGenerator_->Apply(mapped, original, level, currentSource_))
+        {
+          LOG(ERROR) << "Unable to generate an anonymized ID";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+      }
+
+      uidMap_.insert(std::make_pair(std::make_pair(level, original), mapped));
+    }
+    else
+    {
+      mapped = previous->second;
+    }
+
+    return mapped;
+  }
+
+
+  void DicomModification::MapDicomTags(ParsedDicomFile& dicom,
+                                       ResourceType level)
+  {
+    std::auto_ptr<DicomTag> tag;
+
+    switch (level)
+    {
+      case ResourceType_Study:
+        tag.reset(new DicomTag(DICOM_TAG_STUDY_INSTANCE_UID));
+        break;
+
+      case ResourceType_Series:
+        tag.reset(new DicomTag(DICOM_TAG_SERIES_INSTANCE_UID));
+        break;
+
+      case ResourceType_Instance:
+        tag.reset(new DicomTag(DICOM_TAG_SOP_INSTANCE_UID));
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    std::string original;
+    if (!dicom.GetTagValue(original, *tag))
+    {
+      original = "";
+    }
+
+    std::string mapped = MapDicomIdentifier(Toolbox::StripSpaces(original), level);
+
+    dicom.Replace(*tag, mapped, 
+                  false /* don't try and decode data URI scheme for UIDs */, 
+                  DicomReplaceMode_InsertIfAbsent);
+  }
+
+  
+  DicomModification::DicomModification() :
+    removePrivateTags_(false),
+    level_(ResourceType_Instance),
+    allowManualIdentifiers_(true),
+    keepStudyInstanceUid_(false),
+    keepSeriesInstanceUid_(false),
+    updateReferencedRelationships_(true),
+    isAnonymization_(false),
+    identifierGenerator_(NULL)
+  {
+  }
+
+  DicomModification::~DicomModification()
+  {
+    ClearReplacements();
+  }
+
+  void DicomModification::Keep(const DicomTag& tag)
+  {
+    bool wasRemoved = IsRemoved(tag);
+    bool wasCleared = IsCleared(tag);
+    
+    removals_.erase(tag);
+    clearings_.erase(tag);
+
+    bool wasReplaced = CancelReplacement(tag);
+
+    if (tag == DICOM_TAG_STUDY_INSTANCE_UID)
+    {
+      keepStudyInstanceUid_ = true;
+    }
+    else if (tag == DICOM_TAG_SERIES_INSTANCE_UID)
+    {
+      keepSeriesInstanceUid_ = true;
+    }
+    else if (tag.IsPrivate())
+    {
+      privateTagsToKeep_.insert(tag);
+    }
+    else if (!wasRemoved &&
+             !wasReplaced &&
+             !wasCleared)
+    {
+      LOG(WARNING) << "Marking this tag as to be kept has no effect: " << tag.Format();
+    }
+
+    MarkNotOrthancAnonymization();
+  }
+
+  void DicomModification::Remove(const DicomTag& tag)
+  {
+    removals_.insert(tag);
+    clearings_.erase(tag);
+    CancelReplacement(tag);
+    privateTagsToKeep_.erase(tag);
+
+    MarkNotOrthancAnonymization();
+  }
+
+  void DicomModification::Clear(const DicomTag& tag)
+  {
+    removals_.erase(tag);
+    clearings_.insert(tag);
+    CancelReplacement(tag);
+    privateTagsToKeep_.erase(tag);
+
+    MarkNotOrthancAnonymization();
+  }
+
+  bool DicomModification::IsRemoved(const DicomTag& tag) const
+  {
+    return removals_.find(tag) != removals_.end();
+  }
+
+  bool DicomModification::IsCleared(const DicomTag& tag) const
+  {
+    return clearings_.find(tag) != clearings_.end();
+  }
+
+  void DicomModification::Replace(const DicomTag& tag,
+                                  const Json::Value& value,
+                                  bool safeForAnonymization)
+  {
+    clearings_.erase(tag);
+    removals_.erase(tag);
+    privateTagsToKeep_.erase(tag);
+    ReplaceInternal(tag, value);
+
+    if (!safeForAnonymization)
+    {
+      MarkNotOrthancAnonymization();
+    }
+  }
+
+
+  bool DicomModification::IsReplaced(const DicomTag& tag) const
+  {
+    return replacements_.find(tag) != replacements_.end();
+  }
+
+  const Json::Value& DicomModification::GetReplacement(const DicomTag& tag) const
+  {
+    Replacements::const_iterator it = replacements_.find(tag);
+
+    if (it == replacements_.end())
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+    else
+    {
+      return *it->second;
+    } 
+  }
+
+
+  std::string DicomModification::GetReplacementAsString(const DicomTag& tag) const
+  {
+    const Json::Value& json = GetReplacement(tag);
+
+    if (json.type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else
+    {
+      return json.asString();
+    }    
+  }
+
+
+  void DicomModification::SetRemovePrivateTags(bool removed)
+  {
+    removePrivateTags_ = removed;
+
+    if (!removed)
+    {
+      MarkNotOrthancAnonymization();
+    }
+  }
+
+  void DicomModification::SetLevel(ResourceType level)
+  {
+    uidMap_.clear();
+    level_ = level;
+
+    if (level != ResourceType_Patient)
+    {
+      MarkNotOrthancAnonymization();
+    }
+  }
+
+
+  void DicomModification::SetupAnonymization2008()
+  {
+    // This is Table E.1-1 from PS 3.15-2008 - DICOM Part 15: Security and System Management Profiles
+    // https://raw.githubusercontent.com/jodogne/dicom-specification/master/2008/08_15pu.pdf
+    
+    removals_.insert(DicomTag(0x0008, 0x0014));  // Instance Creator UID
+    //removals_.insert(DicomTag(0x0008, 0x0018));  // SOP Instance UID => set in Apply()
+    removals_.insert(DicomTag(0x0008, 0x0050));  // Accession Number
+    removals_.insert(DicomTag(0x0008, 0x0080));  // Institution Name
+    removals_.insert(DicomTag(0x0008, 0x0081));  // Institution Address
+    removals_.insert(DicomTag(0x0008, 0x0090));  // Referring Physician's Name 
+    removals_.insert(DicomTag(0x0008, 0x0092));  // Referring Physician's Address 
+    removals_.insert(DicomTag(0x0008, 0x0094));  // Referring Physician's Telephone Numbers 
+    removals_.insert(DicomTag(0x0008, 0x1010));  // Station Name 
+    removals_.insert(DicomTag(0x0008, 0x1030));  // Study Description 
+    removals_.insert(DicomTag(0x0008, 0x103e));  // Series Description 
+    removals_.insert(DicomTag(0x0008, 0x1040));  // Institutional Department Name 
+    removals_.insert(DicomTag(0x0008, 0x1048));  // Physician(s) of Record 
+    removals_.insert(DicomTag(0x0008, 0x1050));  // Performing Physicians' Name 
+    removals_.insert(DicomTag(0x0008, 0x1060));  // Name of Physician(s) Reading Study 
+    removals_.insert(DicomTag(0x0008, 0x1070));  // Operators' Name 
+    removals_.insert(DicomTag(0x0008, 0x1080));  // Admitting Diagnoses Description 
+    //removals_.insert(DicomTag(0x0008, 0x1155));  // Referenced SOP Instance UID => RelationshipsVisitor
+    removals_.insert(DicomTag(0x0008, 0x2111));  // Derivation Description 
+    //removals_.insert(DicomTag(0x0010, 0x0010));  // Patient's Name => cf. below (*)
+    //removals_.insert(DicomTag(0x0010, 0x0020));  // Patient ID => cf. below (*)
+    removals_.insert(DicomTag(0x0010, 0x0030));  // Patient's Birth Date 
+    removals_.insert(DicomTag(0x0010, 0x0032));  // Patient's Birth Time 
+    removals_.insert(DicomTag(0x0010, 0x0040));  // Patient's Sex 
+    removals_.insert(DicomTag(0x0010, 0x1000));  // Other Patient Ids 
+    removals_.insert(DicomTag(0x0010, 0x1001));  // Other Patient Names 
+    removals_.insert(DicomTag(0x0010, 0x1010));  // Patient's Age 
+    removals_.insert(DicomTag(0x0010, 0x1020));  // Patient's Size 
+    removals_.insert(DicomTag(0x0010, 0x1030));  // Patient's Weight 
+    removals_.insert(DicomTag(0x0010, 0x1090));  // Medical Record Locator 
+    removals_.insert(DicomTag(0x0010, 0x2160));  // Ethnic Group 
+    removals_.insert(DicomTag(0x0010, 0x2180));  // Occupation 
+    removals_.insert(DicomTag(0x0010, 0x21b0));  // Additional Patient's History 
+    removals_.insert(DicomTag(0x0010, 0x4000));  // Patient Comments 
+    removals_.insert(DicomTag(0x0018, 0x1000));  // Device Serial Number 
+    removals_.insert(DicomTag(0x0018, 0x1030));  // Protocol Name 
+    //removals_.insert(DicomTag(0x0020, 0x000d));  // Study Instance UID => set in Apply()
+    //removals_.insert(DicomTag(0x0020, 0x000e));  // Series Instance UID => set in Apply()
+    removals_.insert(DicomTag(0x0020, 0x0010));  // Study ID 
+    //removals_.insert(DicomTag(0x0020, 0x0052));  // Frame of Reference UID => cf. RelationshipsVisitor
+    removals_.insert(DicomTag(0x0020, 0x0200));  // Synchronization Frame of Reference UID 
+    removals_.insert(DicomTag(0x0020, 0x4000));  // Image Comments 
+    removals_.insert(DicomTag(0x0040, 0x0275));  // Request Attributes Sequence 
+    removals_.insert(DicomTag(0x0040, 0xa124));  // UID
+    removals_.insert(DicomTag(0x0040, 0xa730));  // Content Sequence 
+    removals_.insert(DicomTag(0x0088, 0x0140));  // Storage Media File-set UID 
+    //removals_.insert(DicomTag(0x3006, 0x0024));  // Referenced Frame of Reference UID => RelationshipsVisitor
+    //removals_.insert(DicomTag(0x3006, 0x00c2));  // Related Frame of Reference UID => RelationshipsVisitor
+
+    // Some more removals (from the experience of DICOM files at the CHU of Liege)
+    removals_.insert(DicomTag(0x0010, 0x1040));  // Patient's Address
+    removals_.insert(DicomTag(0x0032, 0x1032));  // Requesting Physician
+    removals_.insert(DicomTag(0x0010, 0x2154));  // PatientTelephoneNumbers
+    removals_.insert(DicomTag(0x0010, 0x2000));  // Medical Alerts
+
+    // Set the DeidentificationMethod tag
+    ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2008);
+  }
+  
+
+  void DicomModification::SetupAnonymization2017c()
+  {
+    /**
+     * This is Table E.1-1 from PS 3.15-2017c (DICOM Part 15: Security
+     * and System Management Profiles), "basic profile" column. It was
+     * generated automatically with the
+     * "../Resources/GenerateAnonymizationProfile.py" script.
+     * https://raw.githubusercontent.com/jodogne/dicom-specification/master/2017c/part15.pdf
+     **/
+    
+    // TODO: (50xx,xxxx) with rule X                                 // Curve Data
+    // TODO: (60xx,3000) with rule X                                 // Overlay Data
+    // TODO: (60xx,4000) with rule X                                 // Overlay Comments
+    // Tag (0x0008, 0x0018) is set in Apply()         /* U */        // SOP Instance UID
+    // Tag (0x0008, 0x1140) => RelationshipsVisitor   /* X/Z/U* */   // Referenced Image Sequence
+    // Tag (0x0008, 0x1155) => RelationshipsVisitor   /* U */        // Referenced SOP Instance UID
+    // Tag (0x0008, 0x2112) => RelationshipsVisitor   /* X/Z/U* */   // Source Image Sequence
+    // Tag (0x0010, 0x0010) is set below (*)          /* Z */        // Patient's Name
+    // Tag (0x0010, 0x0020) is set below (*)          /* Z */        // Patient ID
+    // Tag (0x0020, 0x000d) is set in Apply()         /* U */        // Study Instance UID
+    // Tag (0x0020, 0x000e) is set in Apply()         /* U */        // Series Instance UID
+    // Tag (0x0020, 0x0052) => RelationshipsVisitor   /* U */        // Frame of Reference UID
+    // Tag (0x3006, 0x0024) => RelationshipsVisitor   /* U */        // Referenced Frame of Reference UID
+    // Tag (0x3006, 0x00c2) => RelationshipsVisitor   /* U */        // Related Frame of Reference UID
+    clearings_.insert(DicomTag(0x0008, 0x0020));                     // Study Date
+    clearings_.insert(DicomTag(0x0008, 0x0023));  /* Z/D */          // Content Date
+    clearings_.insert(DicomTag(0x0008, 0x0030));                     // Study Time
+    clearings_.insert(DicomTag(0x0008, 0x0033));  /* Z/D */          // Content Time
+    clearings_.insert(DicomTag(0x0008, 0x0050));                     // Accession Number
+    clearings_.insert(DicomTag(0x0008, 0x0090));                     // Referring Physician's Name
+    clearings_.insert(DicomTag(0x0008, 0x009c));                     // Consulting Physician's Name
+    clearings_.insert(DicomTag(0x0010, 0x0030));                     // Patient's Birth Date
+    clearings_.insert(DicomTag(0x0010, 0x0040));                     // Patient's Sex
+    clearings_.insert(DicomTag(0x0018, 0x0010));  /* Z/D */          // Contrast Bolus Agent
+    clearings_.insert(DicomTag(0x0020, 0x0010));                     // Study ID
+    clearings_.insert(DicomTag(0x0040, 0x1101));  /* D */            // Person Identification Code Sequence
+    clearings_.insert(DicomTag(0x0040, 0x2016));                     // Placer Order Number / Imaging Service Request
+    clearings_.insert(DicomTag(0x0040, 0x2017));                     // Filler Order Number / Imaging Service Request
+    clearings_.insert(DicomTag(0x0040, 0xa073));  /* D */            // Verifying Observer Sequence
+    clearings_.insert(DicomTag(0x0040, 0xa075));  /* D */            // Verifying Observer Name
+    clearings_.insert(DicomTag(0x0040, 0xa088));                     // Verifying Observer Identification Code Sequence
+    clearings_.insert(DicomTag(0x0040, 0xa123));  /* D */            // Person Name
+    clearings_.insert(DicomTag(0x0070, 0x0001));  /* D */            // Graphic Annotation Sequence
+    clearings_.insert(DicomTag(0x0070, 0x0084));                     // Content Creator's Name
+    removals_.insert(DicomTag(0x0000, 0x1000));                      // Affected SOP Instance UID
+    removals_.insert(DicomTag(0x0000, 0x1001));   /* TODO UID */     // Requested SOP Instance UID
+    removals_.insert(DicomTag(0x0002, 0x0003));   /* TODO UID */     // Media Storage SOP Instance UID
+    removals_.insert(DicomTag(0x0004, 0x1511));   /* TODO UID */     // Referenced SOP Instance UID in File
+    removals_.insert(DicomTag(0x0008, 0x0014));   /* TODO UID */     // Instance Creator UID
+    removals_.insert(DicomTag(0x0008, 0x0015));                      // Instance Coercion DateTime
+    removals_.insert(DicomTag(0x0008, 0x0021));   /* X/D */          // Series Date
+    removals_.insert(DicomTag(0x0008, 0x0022));   /* X/Z */          // Acquisition Date
+    removals_.insert(DicomTag(0x0008, 0x0024));                      // Overlay Date
+    removals_.insert(DicomTag(0x0008, 0x0025));                      // Curve Date
+    removals_.insert(DicomTag(0x0008, 0x002a));   /* X/D */          // Acquisition DateTime
+    removals_.insert(DicomTag(0x0008, 0x0031));   /* X/D */          // Series Time
+    removals_.insert(DicomTag(0x0008, 0x0032));   /* X/Z */          // Acquisition Time
+    removals_.insert(DicomTag(0x0008, 0x0034));                      // Overlay Time
+    removals_.insert(DicomTag(0x0008, 0x0035));                      // Curve Time
+    removals_.insert(DicomTag(0x0008, 0x0058));   /* TODO UID */     // Failed SOP Instance UID List
+    removals_.insert(DicomTag(0x0008, 0x0080));   /* X/Z/D */        // Institution Name
+    removals_.insert(DicomTag(0x0008, 0x0081));                      // Institution Address
+    removals_.insert(DicomTag(0x0008, 0x0082));   /* X/Z/D */        // Institution Code Sequence
+    removals_.insert(DicomTag(0x0008, 0x0092));                      // Referring Physician's Address
+    removals_.insert(DicomTag(0x0008, 0x0094));                      // Referring Physician's Telephone Numbers
+    removals_.insert(DicomTag(0x0008, 0x0096));                      // Referring Physician Identification Sequence
+    removals_.insert(DicomTag(0x0008, 0x009d));                      // Consulting Physician Identification Sequence
+    removals_.insert(DicomTag(0x0008, 0x0201));                      // Timezone Offset From UTC
+    removals_.insert(DicomTag(0x0008, 0x1010));   /* X/Z/D */        // Station Name
+    removals_.insert(DicomTag(0x0008, 0x1030));                      // Study Description
+    removals_.insert(DicomTag(0x0008, 0x103e));                      // Series Description
+    removals_.insert(DicomTag(0x0008, 0x1040));                      // Institutional Department Name
+    removals_.insert(DicomTag(0x0008, 0x1048));                      // Physician(s) of Record
+    removals_.insert(DicomTag(0x0008, 0x1049));                      // Physician(s) of Record Identification Sequence
+    removals_.insert(DicomTag(0x0008, 0x1050));                      // Performing Physicians' Name
+    removals_.insert(DicomTag(0x0008, 0x1052));                      // Performing Physician Identification Sequence
+    removals_.insert(DicomTag(0x0008, 0x1060));                      // Name of Physician(s) Reading Study
+    removals_.insert(DicomTag(0x0008, 0x1062));                      // Physician(s) Reading Study Identification Sequence
+    removals_.insert(DicomTag(0x0008, 0x1070));   /* X/Z/D */        // Operators' Name
+    removals_.insert(DicomTag(0x0008, 0x1072));   /* X/D */          // Operators' Identification Sequence
+    removals_.insert(DicomTag(0x0008, 0x1080));                      // Admitting Diagnoses Description
+    removals_.insert(DicomTag(0x0008, 0x1084));                      // Admitting Diagnoses Code Sequence
+    removals_.insert(DicomTag(0x0008, 0x1110));   /* X/Z */          // Referenced Study Sequence
+    removals_.insert(DicomTag(0x0008, 0x1111));   /* X/Z/D */        // Referenced Performed Procedure Step Sequence
+    removals_.insert(DicomTag(0x0008, 0x1120));                      // Referenced Patient Sequence
+    removals_.insert(DicomTag(0x0008, 0x1195));   /* TODO UID */     // Transaction UID
+    removals_.insert(DicomTag(0x0008, 0x2111));                      // Derivation Description
+    removals_.insert(DicomTag(0x0008, 0x3010));   /* TODO UID */     // Irradiation Event UID
+    removals_.insert(DicomTag(0x0008, 0x4000));                      // Identifying Comments
+    removals_.insert(DicomTag(0x0010, 0x0021));                      // Issuer of Patient ID
+    removals_.insert(DicomTag(0x0010, 0x0032));                      // Patient's Birth Time
+    removals_.insert(DicomTag(0x0010, 0x0050));                      // Patient's Insurance Plan Code Sequence
+    removals_.insert(DicomTag(0x0010, 0x0101));                      // Patient's Primary Language Code Sequence
+    removals_.insert(DicomTag(0x0010, 0x0102));                      // Patient's Primary Language Modifier Code Sequence
+    removals_.insert(DicomTag(0x0010, 0x1000));                      // Other Patient IDs
+    removals_.insert(DicomTag(0x0010, 0x1001));                      // Other Patient Names
+    removals_.insert(DicomTag(0x0010, 0x1002));                      // Other Patient IDs Sequence
+    removals_.insert(DicomTag(0x0010, 0x1005));                      // Patient's Birth Name
+    removals_.insert(DicomTag(0x0010, 0x1010));                      // Patient's Age
+    removals_.insert(DicomTag(0x0010, 0x1020));                      // Patient's Size
+    removals_.insert(DicomTag(0x0010, 0x1030));                      // Patient's Weight
+    removals_.insert(DicomTag(0x0010, 0x1040));                      // Patient Address
+    removals_.insert(DicomTag(0x0010, 0x1050));                      // Insurance Plan Identification
+    removals_.insert(DicomTag(0x0010, 0x1060));                      // Patient's Mother's Birth Name
+    removals_.insert(DicomTag(0x0010, 0x1080));                      // Military Rank
+    removals_.insert(DicomTag(0x0010, 0x1081));                      // Branch of Service
+    removals_.insert(DicomTag(0x0010, 0x1090));                      // Medical Record Locator
+    removals_.insert(DicomTag(0x0010, 0x1100));                      // Referenced Patient Photo Sequence
+    removals_.insert(DicomTag(0x0010, 0x2000));                      // Medical Alerts
+    removals_.insert(DicomTag(0x0010, 0x2110));                      // Allergies
+    removals_.insert(DicomTag(0x0010, 0x2150));                      // Country of Residence
+    removals_.insert(DicomTag(0x0010, 0x2152));                      // Region of Residence
+    removals_.insert(DicomTag(0x0010, 0x2154));                      // Patient's Telephone Numbers
+    removals_.insert(DicomTag(0x0010, 0x2155));                      // Patient's Telecom Information
+    removals_.insert(DicomTag(0x0010, 0x2160));                      // Ethnic Group
+    removals_.insert(DicomTag(0x0010, 0x2180));                      // Occupation
+    removals_.insert(DicomTag(0x0010, 0x21a0));                      // Smoking Status
+    removals_.insert(DicomTag(0x0010, 0x21b0));                      // Additional Patient's History
+    removals_.insert(DicomTag(0x0010, 0x21c0));                      // Pregnancy Status
+    removals_.insert(DicomTag(0x0010, 0x21d0));                      // Last Menstrual Date
+    removals_.insert(DicomTag(0x0010, 0x21f0));                      // Patient's Religious Preference
+    removals_.insert(DicomTag(0x0010, 0x2203));   /* X/Z */          // Patient Sex Neutered
+    removals_.insert(DicomTag(0x0010, 0x2297));                      // Responsible Person
+    removals_.insert(DicomTag(0x0010, 0x2299));                      // Responsible Organization
+    removals_.insert(DicomTag(0x0010, 0x4000));                      // Patient Comments
+    removals_.insert(DicomTag(0x0018, 0x1000));   /* X/Z/D */        // Device Serial Number
+    removals_.insert(DicomTag(0x0018, 0x1002));   /* TODO UID */     // Device UID
+    removals_.insert(DicomTag(0x0018, 0x1004));                      // Plate ID
+    removals_.insert(DicomTag(0x0018, 0x1005));                      // Generator ID
+    removals_.insert(DicomTag(0x0018, 0x1007));                      // Cassette ID
+    removals_.insert(DicomTag(0x0018, 0x1008));                      // Gantry ID
+    removals_.insert(DicomTag(0x0018, 0x1030));   /* X/D */          // Protocol Name
+    removals_.insert(DicomTag(0x0018, 0x1400));   /* X/D */          // Acquisition Device Processing Description
+    removals_.insert(DicomTag(0x0018, 0x2042));   /* TODO UID */     // Target UID
+    removals_.insert(DicomTag(0x0018, 0x4000));                      // Acquisition Comments
+    removals_.insert(DicomTag(0x0018, 0x700a));   /* X/D */          // Detector ID
+    removals_.insert(DicomTag(0x0018, 0x9424));                      // Acquisition Protocol Description
+    removals_.insert(DicomTag(0x0018, 0x9516));   /* X/D */          // Start Acquisition DateTime
+    removals_.insert(DicomTag(0x0018, 0x9517));   /* X/D */          // End Acquisition DateTime
+    removals_.insert(DicomTag(0x0018, 0xa003));                      // Contribution Description
+    removals_.insert(DicomTag(0x0020, 0x0200));   /* TODO UID */     // Synchronization Frame of Reference UID
+    removals_.insert(DicomTag(0x0020, 0x3401));                      // Modifying Device ID
+    removals_.insert(DicomTag(0x0020, 0x3404));                      // Modifying Device Manufacturer
+    removals_.insert(DicomTag(0x0020, 0x3406));                      // Modified Image Description
+    removals_.insert(DicomTag(0x0020, 0x4000));                      // Image Comments
+    removals_.insert(DicomTag(0x0020, 0x9158));                      // Frame Comments
+    removals_.insert(DicomTag(0x0020, 0x9161));   /* TODO UID */     // Concatenation UID
+    removals_.insert(DicomTag(0x0020, 0x9164));   /* TODO UID */     // Dimension Organization UID
+    removals_.insert(DicomTag(0x0028, 0x1199));   /* TODO UID */     // Palette Color Lookup Table UID
+    removals_.insert(DicomTag(0x0028, 0x1214));   /* TODO UID */     // Large Palette Color Lookup Table UID
+    removals_.insert(DicomTag(0x0028, 0x4000));                      // Image Presentation Comments
+    removals_.insert(DicomTag(0x0032, 0x0012));                      // Study ID Issuer
+    removals_.insert(DicomTag(0x0032, 0x1020));                      // Scheduled Study Location
+    removals_.insert(DicomTag(0x0032, 0x1021));                      // Scheduled Study Location AE Title
+    removals_.insert(DicomTag(0x0032, 0x1030));                      // Reason for Study
+    removals_.insert(DicomTag(0x0032, 0x1032));                      // Requesting Physician
+    removals_.insert(DicomTag(0x0032, 0x1033));                      // Requesting Service
+    removals_.insert(DicomTag(0x0032, 0x1060));   /* X/Z */          // Requested Procedure Description
+    removals_.insert(DicomTag(0x0032, 0x1070));                      // Requested Contrast Agent
+    removals_.insert(DicomTag(0x0032, 0x4000));                      // Study Comments
+    removals_.insert(DicomTag(0x0038, 0x0004));                      // Referenced Patient Alias Sequence
+    removals_.insert(DicomTag(0x0038, 0x0010));                      // Admission ID
+    removals_.insert(DicomTag(0x0038, 0x0011));                      // Issuer of Admission ID
+    removals_.insert(DicomTag(0x0038, 0x001e));                      // Scheduled Patient Institution Residence
+    removals_.insert(DicomTag(0x0038, 0x0020));                      // Admitting Date
+    removals_.insert(DicomTag(0x0038, 0x0021));                      // Admitting Time
+    removals_.insert(DicomTag(0x0038, 0x0040));                      // Discharge Diagnosis Description
+    removals_.insert(DicomTag(0x0038, 0x0050));                      // Special Needs
+    removals_.insert(DicomTag(0x0038, 0x0060));                      // Service Episode ID
+    removals_.insert(DicomTag(0x0038, 0x0061));                      // Issuer of Service Episode ID
+    removals_.insert(DicomTag(0x0038, 0x0062));                      // Service Episode Description
+    removals_.insert(DicomTag(0x0038, 0x0300));                      // Current Patient Location
+    removals_.insert(DicomTag(0x0038, 0x0400));                      // Patient's Institution Residence
+    removals_.insert(DicomTag(0x0038, 0x0500));                      // Patient State
+    removals_.insert(DicomTag(0x0038, 0x4000));                      // Visit Comments
+    removals_.insert(DicomTag(0x0040, 0x0001));                      // Scheduled Station AE Title
+    removals_.insert(DicomTag(0x0040, 0x0002));                      // Scheduled Procedure Step Start Date
+    removals_.insert(DicomTag(0x0040, 0x0003));                      // Scheduled Procedure Step Start Time
+    removals_.insert(DicomTag(0x0040, 0x0004));                      // Scheduled Procedure Step End Date
+    removals_.insert(DicomTag(0x0040, 0x0005));                      // Scheduled Procedure Step End Time
+    removals_.insert(DicomTag(0x0040, 0x0006));                      // Scheduled Performing Physician Name
+    removals_.insert(DicomTag(0x0040, 0x0007));                      // Scheduled Procedure Step Description
+    removals_.insert(DicomTag(0x0040, 0x000b));                      // Scheduled Performing Physician Identification Sequence
+    removals_.insert(DicomTag(0x0040, 0x0010));                      // Scheduled Station Name
+    removals_.insert(DicomTag(0x0040, 0x0011));                      // Scheduled Procedure Step Location
+    removals_.insert(DicomTag(0x0040, 0x0012));                      // Pre-Medication
+    removals_.insert(DicomTag(0x0040, 0x0241));                      // Performed Station AE Title
+    removals_.insert(DicomTag(0x0040, 0x0242));                      // Performed Station Name
+    removals_.insert(DicomTag(0x0040, 0x0243));                      // Performed Location
+    removals_.insert(DicomTag(0x0040, 0x0244));                      // Performed Procedure Step Start Date
+    removals_.insert(DicomTag(0x0040, 0x0245));                      // Performed Procedure Step Start Time
+    removals_.insert(DicomTag(0x0040, 0x0250));                      // Performed Procedure Step End Date
+    removals_.insert(DicomTag(0x0040, 0x0251));                      // Performed Procedure Step End Time
+    removals_.insert(DicomTag(0x0040, 0x0253));                      // Performed Procedure Step ID
+    removals_.insert(DicomTag(0x0040, 0x0254));                      // Performed Procedure Step Description
+    removals_.insert(DicomTag(0x0040, 0x0275));                      // Request Attributes Sequence
+    removals_.insert(DicomTag(0x0040, 0x0280));                      // Comments on the Performed Procedure Step
+    removals_.insert(DicomTag(0x0040, 0x0555));                      // Acquisition Context Sequence
+    removals_.insert(DicomTag(0x0040, 0x1001));                      // Requested Procedure ID
+    removals_.insert(DicomTag(0x0040, 0x1004));                      // Patient Transport Arrangements
+    removals_.insert(DicomTag(0x0040, 0x1005));                      // Requested Procedure Location
+    removals_.insert(DicomTag(0x0040, 0x1010));                      // Names of Intended Recipient of Results
+    removals_.insert(DicomTag(0x0040, 0x1011));                      // Intended Recipients of Results Identification Sequence
+    removals_.insert(DicomTag(0x0040, 0x1102));                      // Person Address
+    removals_.insert(DicomTag(0x0040, 0x1103));                      // Person's Telephone Numbers
+    removals_.insert(DicomTag(0x0040, 0x1104));                      // Person's Telecom Information
+    removals_.insert(DicomTag(0x0040, 0x1400));                      // Requested Procedure Comments
+    removals_.insert(DicomTag(0x0040, 0x2001));                      // Reason for the Imaging Service Request
+    removals_.insert(DicomTag(0x0040, 0x2008));                      // Order Entered By
+    removals_.insert(DicomTag(0x0040, 0x2009));                      // Order Enterer Location
+    removals_.insert(DicomTag(0x0040, 0x2010));                      // Order Callback Phone Number
+    removals_.insert(DicomTag(0x0040, 0x2011));                      // Order Callback Telecom Information
+    removals_.insert(DicomTag(0x0040, 0x2400));                      // Imaging Service Request Comments
+    removals_.insert(DicomTag(0x0040, 0x3001));                      // Confidentiality Constraint on Patient Data Description
+    removals_.insert(DicomTag(0x0040, 0x4005));                      // Scheduled Procedure Step Start DateTime
+    removals_.insert(DicomTag(0x0040, 0x4010));                      // Scheduled Procedure Step Modification DateTime
+    removals_.insert(DicomTag(0x0040, 0x4011));                      // Expected Completion DateTime
+    removals_.insert(DicomTag(0x0040, 0x4023));   /* TODO UID */     // Referenced General Purpose Scheduled Procedure Step Transaction UID
+    removals_.insert(DicomTag(0x0040, 0x4025));                      // Scheduled Station Name Code Sequence
+    removals_.insert(DicomTag(0x0040, 0x4027));                      // Scheduled Station Geographic Location Code Sequence
+    removals_.insert(DicomTag(0x0040, 0x4028));                      // Performed Station Name Code Sequence
+    removals_.insert(DicomTag(0x0040, 0x4030));                      // Performed Station Geographic Location Code Sequence
+    removals_.insert(DicomTag(0x0040, 0x4034));                      // Scheduled Human Performers Sequence
+    removals_.insert(DicomTag(0x0040, 0x4035));                      // Actual Human Performers Sequence
+    removals_.insert(DicomTag(0x0040, 0x4036));                      // Human Performers Organization
+    removals_.insert(DicomTag(0x0040, 0x4037));                      // Human Performers Name
+    removals_.insert(DicomTag(0x0040, 0x4050));                      // Performed Procedure Step Start DateTime
+    removals_.insert(DicomTag(0x0040, 0x4051));                      // Performed Procedure Step End DateTime
+    removals_.insert(DicomTag(0x0040, 0x4052));                      // Procedure Step Cancellation DateTime
+    removals_.insert(DicomTag(0x0040, 0xa027));                      // Verifying Organization
+    removals_.insert(DicomTag(0x0040, 0xa078));                      // Author Observer Sequence
+    removals_.insert(DicomTag(0x0040, 0xa07a));                      // Participant Sequence
+    removals_.insert(DicomTag(0x0040, 0xa07c));                      // Custodial Organization Sequence
+    removals_.insert(DicomTag(0x0040, 0xa124));   /* TODO UID */     // UID
+    removals_.insert(DicomTag(0x0040, 0xa171));   /* TODO UID */     // Observation UID
+    removals_.insert(DicomTag(0x0040, 0xa172));   /* TODO UID */     // Referenced Observation UID (Trial)
+    removals_.insert(DicomTag(0x0040, 0xa192));                      // Observation Date (Trial)
+    removals_.insert(DicomTag(0x0040, 0xa193));                      // Observation Time (Trial)
+    removals_.insert(DicomTag(0x0040, 0xa307));                      // Current Observer (Trial)
+    removals_.insert(DicomTag(0x0040, 0xa352));                      // Verbal Source (Trial)
+    removals_.insert(DicomTag(0x0040, 0xa353));                      // Address (Trial)
+    removals_.insert(DicomTag(0x0040, 0xa354));                      // Telephone Number (Trial)
+    removals_.insert(DicomTag(0x0040, 0xa358));                      // Verbal Source Identifier Code Sequence (Trial)
+    removals_.insert(DicomTag(0x0040, 0xa402));   /* TODO UID */     // Observation Subject UID (Trial)
+    removals_.insert(DicomTag(0x0040, 0xa730));                      // Content Sequence
+    removals_.insert(DicomTag(0x0040, 0xdb0c));   /* TODO UID */     // Template Extension Organization UID
+    removals_.insert(DicomTag(0x0040, 0xdb0d));   /* TODO UID */     // Template Extension Creator UID
+    removals_.insert(DicomTag(0x0062, 0x0021));   /* TODO UID */     // Tracking UID
+    removals_.insert(DicomTag(0x0070, 0x0086));                      // Content Creator's Identification Code Sequence
+    removals_.insert(DicomTag(0x0070, 0x031a));   /* TODO UID */     // Fiducial UID
+    removals_.insert(DicomTag(0x0070, 0x1101));   /* TODO UID */     // Presentation Display Collection UID
+    removals_.insert(DicomTag(0x0070, 0x1102));   /* TODO UID */     // Presentation Sequence Collection UID
+    removals_.insert(DicomTag(0x0088, 0x0140));   /* TODO UID */     // Storage Media File-set UID
+    removals_.insert(DicomTag(0x0088, 0x0200));                      // Icon Image Sequence(see Note 12)
+    removals_.insert(DicomTag(0x0088, 0x0904));                      // Topic Title
+    removals_.insert(DicomTag(0x0088, 0x0906));                      // Topic Subject
+    removals_.insert(DicomTag(0x0088, 0x0910));                      // Topic Author
+    removals_.insert(DicomTag(0x0088, 0x0912));                      // Topic Keywords
+    removals_.insert(DicomTag(0x0400, 0x0100));                      // Digital Signature UID
+    removals_.insert(DicomTag(0x0400, 0x0402));                      // Referenced Digital Signature Sequence
+    removals_.insert(DicomTag(0x0400, 0x0403));                      // Referenced SOP Instance MAC Sequence
+    removals_.insert(DicomTag(0x0400, 0x0404));                      // MAC
+    removals_.insert(DicomTag(0x0400, 0x0550));                      // Modified Attributes Sequence
+    removals_.insert(DicomTag(0x0400, 0x0561));                      // Original Attributes Sequence
+    removals_.insert(DicomTag(0x2030, 0x0020));                      // Text String
+    removals_.insert(DicomTag(0x3008, 0x0105));                      // Source Serial Number
+    removals_.insert(DicomTag(0x300a, 0x0013));   /* TODO UID */     // Dose Reference UID
+    removals_.insert(DicomTag(0x300c, 0x0113));                      // Reason for Omission Description
+    removals_.insert(DicomTag(0x300e, 0x0008));   /* X/Z */          // Reviewer Name
+    removals_.insert(DicomTag(0x4000, 0x0010));                      // Arbitrary
+    removals_.insert(DicomTag(0x4000, 0x4000));                      // Text Comments
+    removals_.insert(DicomTag(0x4008, 0x0042));                      // Results ID Issuer
+    removals_.insert(DicomTag(0x4008, 0x0102));                      // Interpretation Recorder
+    removals_.insert(DicomTag(0x4008, 0x010a));                      // Interpretation Transcriber
+    removals_.insert(DicomTag(0x4008, 0x010b));                      // Interpretation Text
+    removals_.insert(DicomTag(0x4008, 0x010c));                      // Interpretation Author
+    removals_.insert(DicomTag(0x4008, 0x0111));                      // Interpretation Approver Sequence
+    removals_.insert(DicomTag(0x4008, 0x0114));                      // Physician Approving Interpretation
+    removals_.insert(DicomTag(0x4008, 0x0115));                      // Interpretation Diagnosis Description
+    removals_.insert(DicomTag(0x4008, 0x0118));                      // Results Distribution List Sequence
+    removals_.insert(DicomTag(0x4008, 0x0119));                      // Distribution Name
+    removals_.insert(DicomTag(0x4008, 0x011a));                      // Distribution Address
+    removals_.insert(DicomTag(0x4008, 0x0202));                      // Interpretation ID Issuer
+    removals_.insert(DicomTag(0x4008, 0x0300));                      // Impressions
+    removals_.insert(DicomTag(0x4008, 0x4000));                      // Results Comments
+    removals_.insert(DicomTag(0xfffa, 0xfffa));                      // Digital Signatures Sequence
+    removals_.insert(DicomTag(0xfffc, 0xfffc));                      // Data Set Trailing Padding
+    
+    // Set the DeidentificationMethod tag
+    ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2017c);
+  }
+  
+
+  void DicomModification::SetupAnonymization(DicomVersion version)
+  {
+    isAnonymization_ = true;
+    
+    removals_.clear();
+    clearings_.clear();
+    ClearReplacements();
+    removePrivateTags_ = true;
+    level_ = ResourceType_Patient;
+    uidMap_.clear();
+    privateTagsToKeep_.clear();
+
+    switch (version)
+    {
+      case DicomVersion_2008:
+        SetupAnonymization2008();
+        break;
+
+      case DicomVersion_2017c:
+        SetupAnonymization2017c();
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    // Set the PatientIdentityRemoved tag
+    ReplaceInternal(DicomTag(0x0012, 0x0062), "YES");
+
+    // (*) Choose a random patient name and ID
+    std::string patientId = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient);
+    ReplaceInternal(DICOM_TAG_PATIENT_ID, patientId);
+    ReplaceInternal(DICOM_TAG_PATIENT_NAME, patientId);
+  }
+
+  void DicomModification::Apply(ParsedDicomFile& toModify)
+  {
+    // Check the request
+    assert(ResourceType_Patient + 1 == ResourceType_Study &&
+           ResourceType_Study + 1 == ResourceType_Series &&
+           ResourceType_Series + 1 == ResourceType_Instance);
+
+    if (IsRemoved(DICOM_TAG_PATIENT_ID) ||
+        IsRemoved(DICOM_TAG_STUDY_INSTANCE_UID) ||
+        IsRemoved(DICOM_TAG_SERIES_INSTANCE_UID) ||
+        IsRemoved(DICOM_TAG_SOP_INSTANCE_UID))
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+    
+
+    // Sanity checks at the patient level
+    if (level_ == ResourceType_Patient && !IsReplaced(DICOM_TAG_PATIENT_ID))
+    {
+      LOG(ERROR) << "When modifying a patient, her PatientID is required to be modified";
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    if (!allowManualIdentifiers_)
+    {
+      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+      {
+        LOG(ERROR) << "When modifying a patient, the StudyInstanceUID cannot be manually modified";
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+
+      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+      {
+        LOG(ERROR) << "When modifying a patient, the SeriesInstanceUID cannot be manually modified";
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+
+      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
+      {
+        LOG(ERROR) << "When modifying a patient, the SopInstanceUID cannot be manually modified";
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+    }
+
+
+    // Sanity checks at the study level
+    if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_PATIENT_ID))
+    {
+      LOG(ERROR) << "When modifying a study, the parent PatientID cannot be manually modified";
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    if (!allowManualIdentifiers_)
+    {
+      if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+      {
+        LOG(ERROR) << "When modifying a study, the SeriesInstanceUID cannot be manually modified";
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+
+      if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
+      {
+        LOG(ERROR) << "When modifying a study, the SopInstanceUID cannot be manually modified";
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+    }
+
+
+    // Sanity checks at the series level
+    if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_PATIENT_ID))
+    {
+      LOG(ERROR) << "When modifying a series, the parent PatientID cannot be manually modified";
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+    {
+      LOG(ERROR) << "When modifying a series, the parent StudyInstanceUID cannot be manually modified";
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    if (!allowManualIdentifiers_)
+    {
+      if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
+      {
+        LOG(ERROR) << "When modifying a series, the SopInstanceUID cannot be manually modified";
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+    }
+
+
+    // Sanity checks at the instance level
+    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_PATIENT_ID))
+    {
+      LOG(ERROR) << "When modifying an instance, the parent PatientID cannot be manually modified";
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+    {
+      LOG(ERROR) << "When modifying an instance, the parent StudyInstanceUID cannot be manually modified";
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+    {
+      LOG(ERROR) << "When modifying an instance, the parent SeriesInstanceUID cannot be manually modified";
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+
+    // (0) Create a summary of the source file, if a custom generator
+    // is provided
+    if (identifierGenerator_ != NULL)
+    {
+      toModify.ExtractDicomSummary(currentSource_);
+    }
+
+
+    // (1) Remove the private tags, if need be
+    if (removePrivateTags_)
+    {
+      toModify.RemovePrivateTags(privateTagsToKeep_);
+    }
+
+    // (2) Clear the tags specified by the user
+    for (SetOfTags::const_iterator it = clearings_.begin(); 
+         it != clearings_.end(); ++it)
+    {
+      toModify.Clear(*it, true /* only clear if the tag exists in the original file */);
+    }
+
+    // (3) Remove the tags specified by the user
+    for (SetOfTags::const_iterator it = removals_.begin(); 
+         it != removals_.end(); ++it)
+    {
+      toModify.Remove(*it);
+    }
+
+    // (4) Replace the tags
+    for (Replacements::const_iterator it = replacements_.begin(); 
+         it != replacements_.end(); ++it)
+    {
+      toModify.Replace(it->first, *it->second, true /* decode data URI scheme */, DicomReplaceMode_InsertIfAbsent);
+    }
+
+    // (5) Update the DICOM identifiers
+    if (level_ <= ResourceType_Study &&
+        !IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+    {
+      if (keepStudyInstanceUid_)
+      {
+        LOG(WARNING) << "Modifying a study while keeping its original StudyInstanceUID: This should be avoided!";
+      }
+      else
+      {
+        MapDicomTags(toModify, ResourceType_Study);
+      }
+    }
+
+    if (level_ <= ResourceType_Series &&
+        !IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+    {
+      if (keepSeriesInstanceUid_)
+      {
+        LOG(WARNING) << "Modifying a series while keeping its original SeriesInstanceUID: This should be avoided!";
+      }
+      else
+      {
+        MapDicomTags(toModify, ResourceType_Series);
+      }
+    }
+
+    if (level_ <= ResourceType_Instance &&  // Always true
+        !IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
+    {
+      MapDicomTags(toModify, ResourceType_Instance);
+    }
+
+    // (6) Update the "referenced" relationships in the case of an anonymization
+    if (isAnonymization_)
+    {
+      RelationshipsVisitor visitor(*this);
+
+      if (updateReferencedRelationships_)
+      {
+        toModify.Apply(visitor);
+      }
+      else
+      {
+        visitor.RemoveRelationships(toModify);
+      }
+    }
+  }
+
+
+  static bool IsDatabaseKey(const DicomTag& tag)
+  {
+    return (tag == DICOM_TAG_PATIENT_ID ||
+            tag == DICOM_TAG_STUDY_INSTANCE_UID ||
+            tag == DICOM_TAG_SERIES_INSTANCE_UID ||
+            tag == DICOM_TAG_SOP_INSTANCE_UID);
+  }
+
+
+  static void ParseListOfTags(DicomModification& target,
+                              const Json::Value& query,
+                              DicomModification::TagOperation operation,
+                              bool force)
+  {
+    if (!query.isArray())
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    for (Json::Value::ArrayIndex i = 0; i < query.size(); i++)
+    {
+      if (query[i].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+      
+      std::string name = query[i].asString();
+
+      DicomTag tag = FromDcmtkBridge::ParseTag(name);
+
+      if (!force && IsDatabaseKey(tag))
+      {
+        LOG(ERROR) << "Marking tag \"" << name << "\" as to be "
+                   << (operation == DicomModification::TagOperation_Keep ? "kept" : "removed")
+                   << " requires the \"Force\" option to be set to true";
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+
+      switch (operation)
+      {
+        case DicomModification::TagOperation_Keep:
+          target.Keep(tag);
+          VLOG(1) << "Keep: " << name << " " << tag;
+          break;
+
+        case DicomModification::TagOperation_Remove:
+          target.Remove(tag);
+          VLOG(1) << "Remove: " << name << " " << tag;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  static void ParseReplacements(DicomModification& target,
+                                const Json::Value& replacements,
+                                bool force)
+  {
+    if (!replacements.isObject())
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    Json::Value::Members members = replacements.getMemberNames();
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const std::string& name = members[i];
+      const Json::Value& value = replacements[name];
+
+      DicomTag tag = FromDcmtkBridge::ParseTag(name);
+
+      if (!force && IsDatabaseKey(tag))
+      {
+        LOG(ERROR) << "Marking tag \"" << name << "\" as to be replaced "
+                   << "requires the \"Force\" option to be set to true";
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+
+      target.Replace(tag, value, false);
+
+      VLOG(1) << "Replace: " << name << " " << tag 
+              << " == " << value.toStyledString();
+    }
+  }
+
+
+  static bool GetBooleanValue(const std::string& member,
+                              const Json::Value& json,
+                              bool defaultValue)
+  {
+    if (!json.isMember(member))
+    {
+      return defaultValue;
+    }
+    else if (json[member].type() == Json::booleanValue)
+    {
+      return json[member].asBool();
+    }
+    else
+    {
+      LOG(ERROR) << "Member \"" << member << "\" should be a Boolean value";
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  void DicomModification::ParseModifyRequest(const Json::Value& request)
+  {
+    if (!request.isObject())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    bool force = GetBooleanValue("Force", request, false);
+      
+    if (GetBooleanValue("RemovePrivateTags", request, false))
+    {
+      SetRemovePrivateTags(true);
+    }
+
+    if (request.isMember("Remove"))
+    {
+      ParseListOfTags(*this, request["Remove"], TagOperation_Remove, force);
+    }
+
+    if (request.isMember("Replace"))
+    {
+      ParseReplacements(*this, request["Replace"], force);
+    }
+
+    // The "Keep" operation only makes sense for the tags
+    // StudyInstanceUID, SeriesInstanceUID and SOPInstanceUID. Avoid
+    // this feature as much as possible, as this breaks the DICOM
+    // model of the real world, except if you know exactly what
+    // you're doing!
+    if (request.isMember("Keep"))
+    {
+      ParseListOfTags(*this, request["Keep"], TagOperation_Keep, force);
+    }
+  }
+
+
+  void DicomModification::ParseAnonymizationRequest(bool& patientNameReplaced,
+                                                    const Json::Value& request)
+  {
+    if (!request.isObject())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    bool force = GetBooleanValue("Force", request, false);
+      
+    // As of Orthanc 1.3.0, the default anonymization is done
+    // according to PS 3.15-2017c Table E.1-1 (basic profile)
+    DicomVersion version = DicomVersion_2017c;
+    if (request.isMember("DicomVersion"))
+    {
+      if (request["DicomVersion"].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        version = StringToDicomVersion(request["DicomVersion"].asString());
+      }
+    }
+        
+    SetupAnonymization(version);
+
+    std::string patientName = GetReplacementAsString(DICOM_TAG_PATIENT_NAME);    
+
+    if (GetBooleanValue("KeepPrivateTags", request, false))
+    {
+      SetRemovePrivateTags(false);
+    }
+
+    if (request.isMember("Remove"))
+    {
+      ParseListOfTags(*this, request["Remove"], TagOperation_Remove, force);
+    }
+
+    if (request.isMember("Replace"))
+    {
+      ParseReplacements(*this, request["Replace"], force);
+    }
+
+    if (request.isMember("Keep"))
+    {
+      ParseListOfTags(*this, request["Keep"], TagOperation_Keep, force);
+    }
+
+    patientNameReplaced = (IsReplaced(DICOM_TAG_PATIENT_NAME) &&
+                           GetReplacement(DICOM_TAG_PATIENT_NAME) == patientName);
+  }
+
+
+
+
+  static const char* REMOVE_PRIVATE_TAGS = "RemovePrivateTags";
+  static const char* LEVEL = "Level";
+  static const char* ALLOW_MANUAL_IDENTIFIERS = "AllowManualIdentifiers";
+  static const char* KEEP_STUDY_INSTANCE_UID = "KeepStudyInstanceUID";
+  static const char* KEEP_SERIES_INSTANCE_UID = "KeepSeriesInstanceUID";
+  static const char* UPDATE_REFERENCED_RELATIONSHIPS = "UpdateReferencedRelationships";
+  static const char* IS_ANONYMIZATION = "IsAnonymization";
+  static const char* REMOVALS = "Removals";
+  static const char* CLEARINGS = "Clearings";
+  static const char* PRIVATE_TAGS_TO_KEEP = "PrivateTagsToKeep";
+  static const char* REPLACEMENTS = "Replacements";
+  static const char* MAP_PATIENTS = "MapPatients";
+  static const char* MAP_STUDIES = "MapStudies";
+  static const char* MAP_SERIES = "MapSeries";
+  static const char* MAP_INSTANCES = "MapInstances";
+  
+  void DicomModification::Serialize(Json::Value& value) const
+  {
+    if (identifierGenerator_ != NULL)
+    {
+      LOG(ERROR) << "Cannot serialize a DicomModification with a custom identifier generator";
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    value = Json::objectValue;
+    value[REMOVE_PRIVATE_TAGS] = removePrivateTags_;
+    value[LEVEL] = EnumerationToString(level_);
+    value[ALLOW_MANUAL_IDENTIFIERS] = allowManualIdentifiers_;
+    value[KEEP_STUDY_INSTANCE_UID] = keepStudyInstanceUid_;
+    value[KEEP_SERIES_INSTANCE_UID] = keepSeriesInstanceUid_;
+    value[UPDATE_REFERENCED_RELATIONSHIPS] = updateReferencedRelationships_;
+    value[IS_ANONYMIZATION] = isAnonymization_;
+
+    SerializationToolbox::WriteSetOfTags(value, removals_, REMOVALS);
+    SerializationToolbox::WriteSetOfTags(value, clearings_, CLEARINGS);
+    SerializationToolbox::WriteSetOfTags(value, privateTagsToKeep_, PRIVATE_TAGS_TO_KEEP);
+
+    Json::Value& tmp = value[REPLACEMENTS];
+
+    tmp = Json::objectValue;
+
+    for (Replacements::const_iterator it = replacements_.begin();
+         it != replacements_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      tmp[it->first.Format()] = *it->second;
+    }
+
+    Json::Value& mapPatients = value[MAP_PATIENTS];
+    Json::Value& mapStudies = value[MAP_STUDIES];
+    Json::Value& mapSeries = value[MAP_SERIES];
+    Json::Value& mapInstances = value[MAP_INSTANCES];
+
+    mapPatients = Json::objectValue;
+    mapStudies = Json::objectValue;
+    mapSeries = Json::objectValue;
+    mapInstances = Json::objectValue;
+
+    for (UidMap::const_iterator it = uidMap_.begin(); it != uidMap_.end(); ++it)
+    {
+      Json::Value* tmp = NULL;
+
+      switch (it->first.first)
+      {
+        case ResourceType_Patient:
+          tmp = &mapPatients;
+          break;
+
+        case ResourceType_Study:
+          tmp = &mapStudies;
+          break;
+
+        case ResourceType_Series:
+          tmp = &mapSeries;
+          break;
+
+        case ResourceType_Instance:
+          tmp = &mapInstances;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      assert(tmp != NULL);
+      (*tmp) [it->first.second] = it->second;
+    }
+  }
+
+
+  void DicomModification::UnserializeUidMap(ResourceType level,
+                                            const Json::Value& serialized,
+                                            const char* field)
+  {
+    if (!serialized.isMember(field) ||
+        serialized[field].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    Json::Value::Members names = serialized[field].getMemberNames();
+    
+    for (Json::Value::Members::const_iterator it = names.begin(); it != names.end(); ++it)
+    {
+      const Json::Value& value = serialized[field][*it];
+
+      if (value.type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        uidMap_[std::make_pair(level, *it)] = value.asString();
+      }
+    }
+  }
+
+  
+  DicomModification::DicomModification(const Json::Value& serialized) :
+    identifierGenerator_(NULL)
+  {
+    removePrivateTags_ = SerializationToolbox::ReadBoolean(serialized, REMOVE_PRIVATE_TAGS);
+    level_ = StringToResourceType(SerializationToolbox::ReadString(serialized, LEVEL).c_str());
+    allowManualIdentifiers_ = SerializationToolbox::ReadBoolean(serialized, ALLOW_MANUAL_IDENTIFIERS);
+    keepStudyInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_STUDY_INSTANCE_UID);
+    keepSeriesInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SERIES_INSTANCE_UID);
+    updateReferencedRelationships_ = SerializationToolbox::ReadBoolean
+      (serialized, UPDATE_REFERENCED_RELATIONSHIPS);
+    isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION);
+
+    SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS);
+    SerializationToolbox::ReadSetOfTags(clearings_, serialized, CLEARINGS);
+    SerializationToolbox::ReadSetOfTags(privateTagsToKeep_, serialized, PRIVATE_TAGS_TO_KEEP);
+
+    if (!serialized.isMember(REPLACEMENTS) ||
+        serialized[REPLACEMENTS].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    Json::Value::Members names = serialized[REPLACEMENTS].getMemberNames();
+
+    for (Json::Value::Members::const_iterator it = names.begin(); it != names.end(); ++it)
+    {
+      DicomTag tag(0, 0);
+      if (!DicomTag::ParseHexadecimal(tag, it->c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        const Json::Value& value = serialized[REPLACEMENTS][*it];
+        replacements_.insert(std::make_pair(tag, new Json::Value(value)));
+      }
+    }
+
+    UnserializeUidMap(ResourceType_Patient, serialized, MAP_PATIENTS);
+    UnserializeUidMap(ResourceType_Study, serialized, MAP_STUDIES);
+    UnserializeUidMap(ResourceType_Series, serialized, MAP_SERIES);
+    UnserializeUidMap(ResourceType_Instance, serialized, MAP_INSTANCES);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/DicomModification.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,185 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ParsedDicomFile.h"
+
+namespace Orthanc
+{
+  class DicomModification : public boost::noncopyable
+  {
+    /**
+     * Process:
+     * (1) Remove private tags
+     * (2) Remove tags specified by the user
+     * (3) Replace tags
+     **/
+
+  public:
+    enum TagOperation
+    {
+      TagOperation_Keep,
+      TagOperation_Remove
+    };
+
+    class IDicomIdentifierGenerator : public boost::noncopyable
+    {
+    public:
+      virtual ~IDicomIdentifierGenerator()
+      {
+      }
+
+      virtual bool Apply(std::string& target,
+                         const std::string& sourceIdentifier,
+                         ResourceType level,
+                         const DicomMap& sourceDicom) = 0;                       
+    };
+
+  private:
+    class RelationshipsVisitor;
+
+    typedef std::set<DicomTag> SetOfTags;
+    typedef std::map<DicomTag, Json::Value*> Replacements;
+    typedef std::map< std::pair<ResourceType, std::string>, std::string>  UidMap;
+
+    SetOfTags removals_;
+    SetOfTags clearings_;
+    Replacements replacements_;
+    bool removePrivateTags_;
+    ResourceType level_;
+    UidMap uidMap_;
+    SetOfTags privateTagsToKeep_;
+    bool allowManualIdentifiers_;
+    bool keepStudyInstanceUid_;
+    bool keepSeriesInstanceUid_;
+    bool updateReferencedRelationships_;
+    bool isAnonymization_;
+    DicomMap currentSource_;
+
+    IDicomIdentifierGenerator* identifierGenerator_;
+
+    std::string MapDicomIdentifier(const std::string& original,
+                                   ResourceType level);
+
+    void MapDicomTags(ParsedDicomFile& dicom,
+                      ResourceType level);
+
+    void MarkNotOrthancAnonymization();
+
+    void ClearReplacements();
+
+    bool CancelReplacement(const DicomTag& tag);
+
+    void ReplaceInternal(const DicomTag& tag,
+                         const Json::Value& value);
+
+    void SetupAnonymization2008();
+
+    void SetupAnonymization2017c();
+
+    void UnserializeUidMap(ResourceType level,
+                           const Json::Value& serialized,
+                           const char* field);
+
+  public:
+    DicomModification();
+
+    DicomModification(const Json::Value& serialized);
+
+    ~DicomModification();
+
+    void Keep(const DicomTag& tag);
+
+    void Remove(const DicomTag& tag);
+
+    // Replace the DICOM tag as a NULL/empty value (e.g. for anonymization)
+    void Clear(const DicomTag& tag);
+
+    bool IsRemoved(const DicomTag& tag) const;
+
+    bool IsCleared(const DicomTag& tag) const;
+
+    // "safeForAnonymization" tells Orthanc that this replacement does
+    // not break the anonymization process it implements (for internal use only)
+    void Replace(const DicomTag& tag,
+                 const Json::Value& value,   // Encoded using UTF-8
+                 bool safeForAnonymization);
+
+    bool IsReplaced(const DicomTag& tag) const;
+
+    const Json::Value& GetReplacement(const DicomTag& tag) const;
+
+    std::string GetReplacementAsString(const DicomTag& tag) const;
+
+    void SetRemovePrivateTags(bool removed);
+
+    bool ArePrivateTagsRemoved() const
+    {
+      return removePrivateTags_;
+    }
+
+    void SetLevel(ResourceType level);
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void SetupAnonymization(DicomVersion version);
+
+    void Apply(ParsedDicomFile& toModify);
+
+    void SetAllowManualIdentifiers(bool check)
+    {
+      allowManualIdentifiers_ = check;
+    }
+
+    bool AreAllowManualIdentifiers() const
+    {
+      return allowManualIdentifiers_;
+    }
+
+    void ParseModifyRequest(const Json::Value& request);
+
+    void ParseAnonymizationRequest(bool& patientNameReplaced,
+                                   const Json::Value& request);
+
+    void SetDicomIdentifierGenerator(IDicomIdentifierGenerator& generator)
+    {
+      identifierGenerator_ = &generator;
+    }
+
+    void Serialize(Json::Value& value) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/FromDcmtkBridge.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,2434 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#include "FromDcmtkBridge.h"
+#include "ToDcmtkBridge.h"
+#include "../Logging.h"
+#include "../Toolbox.h"
+#include "../OrthancException.h"
+
+#if ORTHANC_SANDBOXED == 0
+#  include "../TemporaryFile.h"
+#endif
+
+#include <list>
+#include <limits>
+
+#include <boost/lexical_cast.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/algorithm/string/join.hpp>
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcdicent.h>
+#include <dcmtk/dcmdata/dcdict.h>
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcostrmb.h>
+#include <dcmtk/dcmdata/dcpixel.h>
+#include <dcmtk/dcmdata/dcuid.h>
+#include <dcmtk/dcmdata/dcistrmb.h>
+
+#include <dcmtk/dcmdata/dcvrae.h>
+#include <dcmtk/dcmdata/dcvras.h>
+#include <dcmtk/dcmdata/dcvrat.h>
+#include <dcmtk/dcmdata/dcvrcs.h>
+#include <dcmtk/dcmdata/dcvrda.h>
+#include <dcmtk/dcmdata/dcvrds.h>
+#include <dcmtk/dcmdata/dcvrdt.h>
+#include <dcmtk/dcmdata/dcvrfd.h>
+#include <dcmtk/dcmdata/dcvrfl.h>
+#include <dcmtk/dcmdata/dcvris.h>
+#include <dcmtk/dcmdata/dcvrlo.h>
+#include <dcmtk/dcmdata/dcvrlt.h>
+#include <dcmtk/dcmdata/dcvrpn.h>
+#include <dcmtk/dcmdata/dcvrsh.h>
+#include <dcmtk/dcmdata/dcvrsl.h>
+#include <dcmtk/dcmdata/dcvrss.h>
+#include <dcmtk/dcmdata/dcvrst.h>
+#include <dcmtk/dcmdata/dcvrtm.h>
+#include <dcmtk/dcmdata/dcvrui.h>
+#include <dcmtk/dcmdata/dcvrul.h>
+#include <dcmtk/dcmdata/dcvrus.h>
+#include <dcmtk/dcmdata/dcvrut.h>
+
+#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
+#  include <EmbeddedResources.h>
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+#  include <dcmtk/dcmjpeg/djdecode.h>
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+#  include <dcmtk/dcmjpls/djdecode.h>
+#endif
+
+
+namespace Orthanc
+{
+#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
+  static void LoadEmbeddedDictionary(DcmDataDictionary& dictionary,
+                                     EmbeddedResources::FileResourceId resource)
+  {
+    std::string content;
+    EmbeddedResources::GetFileResource(content, resource);
+
+#if ORTHANC_SANDBOXED == 0
+    TemporaryFile tmp;
+    tmp.Write(content);
+
+    if (!dictionary.loadDictionary(tmp.GetPath().c_str()))
+    {
+      LOG(ERROR) << "Cannot read embedded dictionary. Under Windows, make sure that " 
+                 << "your TEMP directory does not contain special characters.";
+      throw OrthancException(ErrorCode_InternalError);
+    }
+#else
+    if (!dictionary.loadFromMemory(content))
+    {
+      LOG(ERROR) << "Cannot read embedded dictionary. Under Windows, make sure that " 
+                 << "your TEMP directory does not contain special characters.";
+      throw OrthancException(ErrorCode_InternalError);
+    }
+#endif
+  }
+                             
+#else
+  static void LoadExternalDictionary(DcmDataDictionary& dictionary,
+                                     const std::string& directory,
+                                     const std::string& filename)
+  {
+    boost::filesystem::path p = directory;
+    p = p / filename;
+
+    LOG(WARNING) << "Loading the external DICOM dictionary " << p;
+
+    if (!dictionary.loadDictionary(p.string().c_str()))
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+#endif
+
+
+  namespace
+  {
+    class DictionaryLocker
+    {
+    private:
+      DcmDataDictionary& dictionary_;
+
+    public:
+      DictionaryLocker() : dictionary_(dcmDataDict.wrlock())
+      {
+      }
+
+      ~DictionaryLocker()
+      {
+        dcmDataDict.unlock();
+      }
+
+      DcmDataDictionary& operator*()
+      {
+        return dictionary_;
+      }
+
+      DcmDataDictionary* operator->()
+      {
+        return &dictionary_;
+      }
+    };
+
+    
+#define DCMTK_TO_CTYPE_CONVERTER(converter, cType, dcmtkType, getter) \
+ \
+    struct converter \
+    { \
+      typedef cType CType; \
+ \
+      static bool Apply(CType& result, \
+                        DcmElement& element, \
+                        size_t i) \
+      { \
+        return dynamic_cast<dcmtkType&>(element).getter(result, i).good(); \
+      } \
+    };
+
+DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint32Converter, Sint32, DcmSignedLong, getSint32)
+DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint16Converter, Sint16, DcmSignedShort, getSint16)
+DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint32Converter, Uint32, DcmUnsignedLong, getUint32)
+DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint16Converter, Uint16, DcmUnsignedShort, getUint16)
+DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat32Converter, Float32, DcmFloatingPointSingle, getFloat32)
+DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDouble, getFloat64)
+
+
+    template <typename F>
+    static DicomValue* ApplyDcmtkToCTypeConverter(DcmElement& element)
+    {
+      F f;
+      typename F::CType value;
+
+      if (element.getLength() > sizeof(typename F::CType)
+          && (element.getLength() % sizeof(typename F::CType)) == 0)
+      {
+        size_t count = element.getLength() / sizeof(typename F::CType);
+        std::vector<std::string> strings;
+        for (size_t i = 0; i < count; i++) {
+          if (f.Apply(value, element, i)) {
+            strings.push_back(boost::lexical_cast<std::string>(value));
+          }
+        }
+        return new DicomValue(boost::algorithm::join(strings, "\\"), false);
+      }
+      else if (f.Apply(value, element, 0)) {
+        return new DicomValue(boost::lexical_cast<std::string>(value), false);
+      }
+      else {
+        return new DicomValue;
+      }
+    }
+
+  }
+
+
+  void FromDcmtkBridge::InitializeDictionary(bool loadPrivateDictionary)
+  {
+    LOG(INFO) << "Using DCTMK version: " << DCMTK_VERSION_NUMBER;
+    
+    {
+      DictionaryLocker locker;
+
+      locker->clear();
+
+#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
+      LOG(INFO) << "Loading the embedded dictionaries";
+      /**
+       * Do not load DICONDE dictionary, it breaks the other tags. The
+       * command "strace storescu 2>&1 |grep dic" shows that DICONDE
+       * dictionary is not loaded by storescu.
+       **/
+      //LoadEmbeddedDictionary(*locker, EmbeddedResources::DICTIONARY_DICONDE);
+
+      LoadEmbeddedDictionary(*locker, EmbeddedResources::DICTIONARY_DICOM);
+
+      if (loadPrivateDictionary)
+      {
+        LOG(INFO) << "Loading the embedded dictionary of private tags";
+        LoadEmbeddedDictionary(*locker, EmbeddedResources::DICTIONARY_PRIVATE);
+      }
+      else
+      {
+        LOG(INFO) << "The dictionary of private tags has not been loaded";
+      }
+
+#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__)
+      std::string path = DCMTK_DICTIONARY_DIR;
+
+      const char* env = std::getenv(DCM_DICT_ENVIRONMENT_VARIABLE);
+      if (env != NULL)
+      {
+        path = std::string(env);
+      }
+
+      LoadExternalDictionary(*locker, path, "dicom.dic");
+
+      if (loadPrivateDictionary)
+      {
+        LoadExternalDictionary(*locker, path, "private.dic");
+      }
+      else
+      {
+        LOG(INFO) << "The dictionary of private tags has not been loaded";
+      }
+
+#else
+#error Support your platform here
+#endif
+    }
+
+    /* make sure data dictionary is loaded */
+    if (!dcmDataDict.isDictionaryLoaded())
+    {
+      LOG(ERROR) << "No DICOM dictionary loaded, check environment variable: " << DCM_DICT_ENVIRONMENT_VARIABLE;
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    {
+      // Test the dictionary with a simple DICOM tag
+      DcmTag key(0x0010, 0x1030); // This is PatientWeight
+      if (key.getEVR() != EVR_DS)
+      {
+        LOG(ERROR) << "The DICOM dictionary has not been correctly read";
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  void FromDcmtkBridge::RegisterDictionaryTag(const DicomTag& tag,
+                                              ValueRepresentation vr,
+                                              const std::string& name,
+                                              unsigned int minMultiplicity,
+                                              unsigned int maxMultiplicity,
+                                              const std::string& privateCreator)
+  {
+    if (minMultiplicity < 1)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    bool arbitrary = false;
+    if (maxMultiplicity == 0)
+    {
+      maxMultiplicity = DcmVariableVM;
+      arbitrary = true;
+    }
+    else if (maxMultiplicity < minMultiplicity)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    
+    DcmEVR evr = ToDcmtkBridge::Convert(vr);
+
+    LOG(INFO) << "Registering tag in dictionary: " << tag << " " << (DcmVR(evr).getValidVRName()) << " " 
+              << name << " (multiplicity: " << minMultiplicity << "-" 
+              << (arbitrary ? "n" : boost::lexical_cast<std::string>(maxMultiplicity)) << ")";
+
+    std::auto_ptr<DcmDictEntry>  entry;
+    if (privateCreator.empty())
+    {
+      if (tag.GetGroup() % 2 == 1)
+      {
+        char buf[128];
+        sprintf(buf, "Warning: You are registering a private tag (%04x,%04x), "
+                "but no private creator was associated with it", 
+                tag.GetGroup(), tag.GetElement());
+        LOG(WARNING) << buf;
+      }
+
+      entry.reset(new DcmDictEntry(tag.GetGroup(),
+                                   tag.GetElement(),
+                                   evr, name.c_str(),
+                                   static_cast<int>(minMultiplicity),
+                                   static_cast<int>(maxMultiplicity),
+                                   NULL    /* version */,
+                                   OFTrue  /* doCopyString */,
+                                   NULL    /* private creator */));
+    }
+    else
+    {
+      // "Private Data Elements have an odd Group Number that is not
+      // (0001,eeee), (0003,eeee), (0005,eeee), (0007,eeee), or
+      // (FFFF,eeee)."
+      if (tag.GetGroup() % 2 == 0 /* even */ ||
+          tag.GetGroup() == 0x0001 ||
+          tag.GetGroup() == 0x0003 ||
+          tag.GetGroup() == 0x0005 ||
+          tag.GetGroup() == 0x0007 ||
+          tag.GetGroup() == 0xffff)
+      {
+        char buf[128];
+        sprintf(buf, "Trying to register private tag (%04x,%04x), but it must have an odd group >= 0x0009",
+                tag.GetGroup(), tag.GetElement());
+        LOG(ERROR) << buf;
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      entry.reset(new DcmDictEntry(tag.GetGroup(),
+                                   tag.GetElement(),
+                                   evr, name.c_str(),
+                                   static_cast<int>(minMultiplicity),
+                                   static_cast<int>(maxMultiplicity),
+                                   "private" /* version */,
+                                   OFTrue    /* doCopyString */,
+                                   privateCreator.c_str()));
+    }
+
+    entry->setGroupRangeRestriction(DcmDictRange_Unspecified);
+    entry->setElementRangeRestriction(DcmDictRange_Unspecified);
+
+    {
+      DictionaryLocker locker;
+
+      if (locker->findEntry(name.c_str()))
+      {
+        LOG(ERROR) << "Cannot register two tags with the same symbolic name \"" << name << "\"";
+        throw OrthancException(ErrorCode_AlreadyExistingTag);
+      }
+
+      locker->addEntry(entry.release());
+    }
+  }
+
+
+  Encoding FromDcmtkBridge::DetectEncoding(DcmItem& dataset,
+                                           Encoding defaultEncoding)
+  {
+    Encoding encoding = defaultEncoding;
+
+    OFString tmp;
+    if (dataset.findAndGetOFString(DCM_SpecificCharacterSet, tmp).good())
+    {
+      std::string characterSet = Toolbox::StripSpaces(std::string(tmp.c_str()));
+
+      if (characterSet.empty())
+      {
+        // Empty specific character set tag: Use the default encoding
+      }
+      else if (GetDicomEncoding(encoding, characterSet.c_str()))
+      {
+        // The specific character set is supported by the Orthanc core
+      }
+      else
+      {
+        LOG(WARNING) << "Value of Specific Character Set (0008,0005) is not supported: " << characterSet
+                     << ", fallback to ASCII (remove all special characters)";
+        encoding = Encoding_Ascii;
+      }
+    }
+    else
+    {
+      // No specific character set tag: Use the default encoding
+    }
+
+    return encoding;
+  }
+
+
+  void FromDcmtkBridge::ExtractDicomSummary(DicomMap& target, 
+                                            DcmItem& dataset,
+                                            unsigned int maxStringLength,
+                                            Encoding defaultEncoding)
+  {
+    std::set<DicomTag> ignoreTagLength;
+    
+    Encoding encoding = DetectEncoding(dataset, defaultEncoding);
+
+    target.Clear();
+    for (unsigned long i = 0; i < dataset.card(); i++)
+    {
+      DcmElement* element = dataset.getElement(i);
+      if (element && element->isLeaf())
+      {
+        target.SetValue(element->getTag().getGTag(),
+                        element->getTag().getETag(),
+                        ConvertLeafElement(*element, DicomToJsonFlags_Default,
+                                           maxStringLength, encoding, ignoreTagLength));
+      }
+    }
+  }
+
+
+  DicomTag FromDcmtkBridge::Convert(const DcmTag& tag)
+  {
+    return DicomTag(tag.getGTag(), tag.getETag());
+  }
+
+
+  DicomTag FromDcmtkBridge::GetTag(const DcmElement& element)
+  {
+    return DicomTag(element.getGTag(), element.getETag());
+  }
+
+
+  DicomValue* FromDcmtkBridge::ConvertLeafElement(DcmElement& element,
+                                                  DicomToJsonFlags flags,
+                                                  unsigned int maxStringLength,
+                                                  Encoding encoding,
+                                                  const std::set<DicomTag>& ignoreTagLength)
+  {
+    if (!element.isLeaf())
+    {
+      // This function is only applicable to leaf elements
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    char *c = NULL;
+    if (element.isaString() &&
+        element.getString(c).good())
+    {
+      if (c == NULL)  // This case corresponds to the empty string
+      {
+        return new DicomValue("", false);
+      }
+      else
+      {
+        std::string s(c);
+        std::string utf8 = Toolbox::ConvertToUtf8(s, encoding);
+
+        if (maxStringLength != 0 &&
+            utf8.size() > maxStringLength &&
+            ignoreTagLength.find(GetTag(element)) == ignoreTagLength.end())
+        {
+          return new DicomValue;  // Too long, create a NULL value
+        }
+        else
+        {
+          return new DicomValue(utf8, false);
+        }
+      }
+    }
+
+
+    if (element.getVR() == EVR_UN)
+    {
+      // Unknown value representation: Lookup in the dictionary. This
+      // is notably the case for private tags registered with the
+      // "Dictionary" configuration option.
+      DictionaryLocker locker;
+      
+      const DcmDictEntry* entry = locker->findEntry(element.getTag().getXTag(), 
+                                                    element.getTag().getPrivateCreator());
+      if (entry != NULL && 
+          entry->getVR().isaString())
+      {
+        Uint8* data = NULL;
+
+        // At (*), we do not try and convert to UTF-8, as nothing says
+        // the encoding of the private tag is the same as that of the
+        // remaining of the DICOM dataset. Only go for ASCII strings.
+
+        if (element.getUint8Array(data) == EC_Normal &&
+            Toolbox::IsAsciiString(data, element.getLength()))   // (*)
+        {
+          if (data == NULL)
+          {
+            return new DicomValue("", false);   // Empty string
+          }
+          else if (maxStringLength != 0 &&
+                   element.getLength() > maxStringLength &&
+                   ignoreTagLength.find(GetTag(element)) == ignoreTagLength.end())
+          {
+            return new DicomValue;  // Too long, create a NULL value
+          }
+          else
+          {
+            std::string s(reinterpret_cast<const char*>(data), element.getLength());
+            return new DicomValue(s, false);
+          }
+        }
+      }
+    }
+
+    
+    try
+    {
+      // http://support.dcmtk.org/docs/dcvr_8h-source.html
+      switch (element.getVR())
+      {
+
+        /**
+         * Deal with binary data (including PixelData).
+         **/
+
+        case EVR_OB:  // other byte
+        case EVR_OF:  // other float
+        case EVR_OW:  // other word
+        case EVR_UN:  // unknown value representation
+        case EVR_ox:  // OB or OW depending on context
+        case EVR_DS:  // decimal string
+        case EVR_IS:  // integer string
+        case EVR_AS:  // age string
+        case EVR_DA:  // date string
+        case EVR_DT:  // date time string
+        case EVR_TM:  // time string
+        case EVR_AE:  // application entity title
+        case EVR_CS:  // code string
+        case EVR_SH:  // short string
+        case EVR_LO:  // long string
+        case EVR_ST:  // short text
+        case EVR_LT:  // long text
+        case EVR_UT:  // unlimited text
+        case EVR_PN:  // person name
+        case EVR_UI:  // unique identifier
+        case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
+        case EVR_UNKNOWN2B:  // used internally for elements with unknown VR with 2-byte length field in explicit VR
+        {
+          if (!(flags & DicomToJsonFlags_ConvertBinaryToNull))
+          {
+            Uint8* data = NULL;
+            if (element.getUint8Array(data) == EC_Normal)
+            {
+              return new DicomValue(reinterpret_cast<const char*>(data), element.getLength(), true);
+            }
+          }
+
+          return new DicomValue;
+        }
+    
+        /**
+         * Numeric types
+         **/ 
+      
+        case EVR_SL:  // signed long
+        {
+          return ApplyDcmtkToCTypeConverter<DcmtkToSint32Converter>(element);
+        }
+
+        case EVR_SS:  // signed short
+        {
+          return ApplyDcmtkToCTypeConverter<DcmtkToSint16Converter>(element);
+        }
+
+        case EVR_UL:  // unsigned long
+        {
+          return ApplyDcmtkToCTypeConverter<DcmtkToUint32Converter>(element);
+        }
+
+        case EVR_US:  // unsigned short
+        {
+          return ApplyDcmtkToCTypeConverter<DcmtkToUint16Converter>(element);
+        }
+
+        case EVR_FL:  // float single-precision
+        {
+          return ApplyDcmtkToCTypeConverter<DcmtkToFloat32Converter>(element);
+        }
+
+        case EVR_FD:  // float double-precision
+        {
+          return ApplyDcmtkToCTypeConverter<DcmtkToFloat64Converter>(element);
+        }
+
+
+        /**
+         * Attribute tag.
+         **/
+
+        case EVR_AT:
+        {
+          DcmTagKey tag;
+          if (dynamic_cast<DcmAttributeTag&>(element).getTagVal(tag, 0).good())
+          {
+            DicomTag t(tag.getGroup(), tag.getElement());
+            return new DicomValue(t.Format(), false);
+          }
+          else
+          {
+            return new DicomValue;
+          }
+        }
+
+
+        /**
+         * Sequence types, should never occur at this point because of
+         * "element.isLeaf()".
+         **/
+
+        case EVR_SQ:  // sequence of items
+          return new DicomValue;
+
+
+          /**
+           * Internal to DCMTK.
+           **/ 
+
+        case EVR_xs:  // SS or US depending on context
+        case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
+        case EVR_na:  // na="not applicable", for data which has no VR
+        case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
+        case EVR_item:  // used internally for items
+        case EVR_metainfo:  // used internally for meta info datasets
+        case EVR_dataset:  // used internally for datasets
+        case EVR_fileFormat:  // used internally for DICOM files
+        case EVR_dicomDir:  // used internally for DICOMDIR objects
+        case EVR_dirRecord:  // used internally for DICOMDIR records
+        case EVR_pixelSQ:  // used internally for pixel sequences in a compressed image
+        case EVR_pixelItem:  // used internally for pixel items in a compressed image
+        case EVR_PixelData:  // used internally for uncompressed pixeld data
+        case EVR_OverlayData:  // used internally for overlay data
+          return new DicomValue;
+
+
+          /**
+           * Default case.
+           **/ 
+
+        default:
+          return new DicomValue;
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      return new DicomValue;
+    }
+    catch (std::bad_cast&)
+    {
+      return new DicomValue;
+    }
+  }
+
+
+  static Json::Value& PrepareNode(Json::Value& parent,
+                                  DcmElement& element,
+                                  DicomToJsonFormat format)
+  {
+    assert(parent.type() == Json::objectValue);
+
+    DicomTag tag(FromDcmtkBridge::GetTag(element));
+    const std::string formattedTag = tag.Format();
+
+    if (format == DicomToJsonFormat_Short)
+    {
+      parent[formattedTag] = Json::nullValue;
+      return parent[formattedTag];
+    }
+
+    // This code gives access to the name of the private tags
+    std::string tagName = FromDcmtkBridge::GetTagName(element);
+    
+    switch (format)
+    {
+      case DicomToJsonFormat_Human:
+        parent[tagName] = Json::nullValue;
+        return parent[tagName];
+
+      case DicomToJsonFormat_Full:
+      {
+        parent[formattedTag] = Json::objectValue;
+        Json::Value& node = parent[formattedTag];
+
+        if (element.isLeaf())
+        {
+          node["Name"] = tagName;
+
+          if (element.getTag().getPrivateCreator() != NULL)
+          {
+            node["PrivateCreator"] = element.getTag().getPrivateCreator();
+          }
+
+          return node;
+        }
+        else
+        {
+          node["Name"] = tagName;
+          node["Type"] = "Sequence";
+          node["Value"] = Json::nullValue;
+          return node["Value"];
+        }
+      }
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static void LeafValueToJson(Json::Value& target,
+                              const DicomValue& value,
+                              DicomToJsonFormat format,
+                              DicomToJsonFlags flags,
+                              unsigned int maxStringLength)
+  {
+    Json::Value* targetValue = NULL;
+    Json::Value* targetType = NULL;
+
+    switch (format)
+    {
+      case DicomToJsonFormat_Short:
+      case DicomToJsonFormat_Human:
+      {
+        assert(target.type() == Json::nullValue);
+        targetValue = &target;
+        break;
+      }      
+
+      case DicomToJsonFormat_Full:
+      {
+        assert(target.type() == Json::objectValue);
+        target["Value"] = Json::nullValue;
+        target["Type"] = Json::nullValue;
+        targetType = &target["Type"];
+        targetValue = &target["Value"];
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(targetValue != NULL);
+    assert(targetValue->type() == Json::nullValue);
+    assert(targetType == NULL || targetType->type() == Json::nullValue);
+
+    if (value.IsNull())
+    {
+      if (targetType != NULL)
+      {
+        *targetType = "Null";
+      }
+    }
+    else if (value.IsBinary())
+    {
+      if (flags & DicomToJsonFlags_ConvertBinaryToAscii)
+      {
+        *targetValue = Toolbox::ConvertToAscii(value.GetContent());
+      }
+      else
+      {
+        std::string s;
+        value.FormatDataUriScheme(s);
+        *targetValue = s;
+      }
+
+      if (targetType != NULL)
+      {
+        *targetType = "Binary";
+      }
+    }
+    else if (maxStringLength == 0 ||
+             value.GetContent().size() <= maxStringLength)
+    {
+      *targetValue = value.GetContent();
+
+      if (targetType != NULL)
+      {
+        *targetType = "String";
+      }
+    }
+    else
+    {
+      if (targetType != NULL)
+      {
+        *targetType = "TooLong";
+      }
+    }
+  }                              
+
+
+  void FromDcmtkBridge::ElementToJson(Json::Value& parent,
+                                      DcmElement& element,
+                                      DicomToJsonFormat format,
+                                      DicomToJsonFlags flags,
+                                      unsigned int maxStringLength,
+                                      Encoding encoding,
+                                      const std::set<DicomTag>& ignoreTagLength)
+  {
+    if (parent.type() == Json::nullValue)
+    {
+      parent = Json::objectValue;
+    }
+
+    assert(parent.type() == Json::objectValue);
+    Json::Value& target = PrepareNode(parent, element, format);
+
+    if (element.isLeaf())
+    {
+      // The "0" below lets "LeafValueToJson()" take care of "TooLong" values
+      std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement
+                                  (element, flags, 0, encoding, ignoreTagLength));
+
+      if (ignoreTagLength.find(GetTag(element)) == ignoreTagLength.end())
+      {
+        LeafValueToJson(target, *v, format, flags, maxStringLength);
+      }
+      else
+      {
+        LeafValueToJson(target, *v, format, flags, 0);
+      }
+    }
+    else
+    {
+      assert(target.type() == Json::nullValue);
+      target = Json::arrayValue;
+
+      // "All subclasses of DcmElement except for DcmSequenceOfItems
+      // are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset
+      // etc. are not." The following dynamic_cast is thus OK.
+      DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(element);
+
+      for (unsigned long i = 0; i < sequence.card(); i++)
+      {
+        DcmItem* child = sequence.getItem(i);
+        Json::Value& v = target.append(Json::objectValue);
+        DatasetToJson(v, *child, format, flags, maxStringLength, encoding, ignoreTagLength);
+      }
+    }
+  }
+
+
+  void FromDcmtkBridge::DatasetToJson(Json::Value& parent,
+                                      DcmItem& item,
+                                      DicomToJsonFormat format,
+                                      DicomToJsonFlags flags,
+                                      unsigned int maxStringLength,
+                                      Encoding encoding,
+                                      const std::set<DicomTag>& ignoreTagLength)
+  {
+    assert(parent.type() == Json::objectValue);
+
+    for (unsigned long i = 0; i < item.card(); i++)
+    {
+      DcmElement* element = item.getElement(i);
+      if (element == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      DicomTag tag(FromDcmtkBridge::Convert(element->getTag()));
+
+      /*element->getTag().isPrivate()*/
+      if (tag.IsPrivate() &&
+          !(flags & DicomToJsonFlags_IncludePrivateTags))    
+      {
+        continue;
+      }
+
+      if (!(flags & DicomToJsonFlags_IncludeUnknownTags))
+      {
+        DictionaryLocker locker;
+        if (locker->findEntry(element->getTag(), NULL) == NULL)
+        {
+          continue;
+        }
+      }
+
+      DcmEVR evr = element->getTag().getEVR();
+      if (evr == EVR_OB ||
+          evr == EVR_OF ||
+          evr == EVR_OW ||
+          evr == EVR_UN ||
+          evr == EVR_ox)
+      {
+        // This is a binary tag
+        if ((tag == DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludePixelData)) ||
+            (tag != DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludeBinary)))
+        {
+          continue;
+        }
+      }
+
+      FromDcmtkBridge::ElementToJson(parent, *element, format, flags,
+                                     maxStringLength, encoding, ignoreTagLength);
+    }
+  }
+
+
+  void FromDcmtkBridge::ExtractDicomAsJson(Json::Value& target, 
+                                           DcmDataset& dataset,
+                                           DicomToJsonFormat format,
+                                           DicomToJsonFlags flags,
+                                           unsigned int maxStringLength,
+                                           Encoding defaultEncoding,
+                                           const std::set<DicomTag>& ignoreTagLength)
+  {
+    Encoding encoding = DetectEncoding(dataset, defaultEncoding);
+
+    target = Json::objectValue;
+    DatasetToJson(target, dataset, format, flags, maxStringLength, encoding, ignoreTagLength);
+  }
+
+
+  void FromDcmtkBridge::ExtractHeaderAsJson(Json::Value& target, 
+                                            DcmMetaInfo& dataset,
+                                            DicomToJsonFormat format,
+                                            DicomToJsonFlags flags,
+                                            unsigned int maxStringLength)
+  {
+    std::set<DicomTag> ignoreTagLength;
+    target = Json::objectValue;
+    DatasetToJson(target, dataset, format, flags, maxStringLength, Encoding_Ascii, ignoreTagLength);
+  }
+
+
+
+  static std::string GetTagNameInternal(DcmTag& tag)
+  {
+    {
+      // Some patches for important tags because of different DICOM
+      // dictionaries between DCMTK versions
+      DicomTag tmp(tag.getGroup(), tag.getElement());
+      std::string n = tmp.GetMainTagsName();
+      if (n.size() != 0)
+      {
+        return n;
+      }
+      // End of patches
+    }
+
+#if 0
+    // This version explicitly calls the dictionary
+    const DcmDataDictionary& dict = dcmDataDict.rdlock();
+    const DcmDictEntry* entry = dict.findEntry(tag, NULL);
+
+    std::string s(DcmTag_ERROR_TagName);
+    if (entry != NULL)
+    {
+      s = std::string(entry->getTagName());
+    }
+
+    dcmDataDict.unlock();
+    return s;
+#else
+    const char* name = tag.getTagName();
+    if (name == NULL)
+    {
+      return DcmTag_ERROR_TagName;
+    }
+    else
+    {
+      return std::string(name);
+    }
+#endif
+  }
+
+
+  std::string FromDcmtkBridge::GetTagName(const DicomTag& t,
+                                          const std::string& privateCreator)
+  {
+    DcmTag tag(t.GetGroup(), t.GetElement());
+
+    if (!privateCreator.empty())
+    {
+      tag.setPrivateCreator(privateCreator.c_str());
+    }
+
+    return GetTagNameInternal(tag);
+  }
+
+
+  std::string FromDcmtkBridge::GetTagName(const DcmElement& element)
+  {
+    // Copy the tag to ensure const-correctness of DcmElement. Note
+    // that the private creator information is also copied.
+    DcmTag tag(element.getTag());  
+
+    return GetTagNameInternal(tag);
+  }
+
+
+
+  DicomTag FromDcmtkBridge::ParseTag(const char* name)
+  {
+    DicomTag parsed(0, 0);
+    if (DicomTag::ParseHexadecimal(parsed, name))
+    {
+      return parsed;
+    }
+
+#if 0
+    const DcmDataDictionary& dict = dcmDataDict.rdlock();
+    const DcmDictEntry* entry = dict.findEntry(name);
+
+    if (entry == NULL)
+    {
+      dcmDataDict.unlock();
+      throw OrthancException(ErrorCode_UnknownDicomTag);
+    }
+    else
+    {
+      DcmTagKey key = entry->getKey();
+      DicomTag tag(key.getGroup(), key.getElement());
+      dcmDataDict.unlock();
+      return tag;
+    }
+#else
+    DcmTag tag;
+    if (DcmTag::findTagFromName(name, tag).good())
+    {
+      return DicomTag(tag.getGTag(), tag.getETag());
+    }
+    else
+    {
+      LOG(INFO) << "Unknown DICOM tag: \"" << name << "\"";
+      throw OrthancException(ErrorCode_UnknownDicomTag);
+    }
+#endif
+  }
+
+
+  bool FromDcmtkBridge::IsUnknownTag(const DicomTag& tag)
+  {
+    DcmTag tmp(tag.GetGroup(), tag.GetElement());
+    return tmp.isUnknownVR();
+  }
+
+
+  void FromDcmtkBridge::ToJson(Json::Value& result,
+                               const DicomMap& values,
+                               bool simplify)
+  {
+    if (result.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    result.clear();
+
+    for (DicomMap::Map::const_iterator 
+           it = values.map_.begin(); it != values.map_.end(); ++it)
+    {
+      // TODO Inject PrivateCreator if some is available in the DicomMap?
+      const std::string tagName = GetTagName(it->first, "");
+
+      if (simplify)
+      {
+        if (it->second->IsNull())
+        {
+          result[tagName] = Json::nullValue;
+        }
+        else
+        {
+          // TODO IsBinary
+          result[tagName] = it->second->GetContent();
+        }
+      }
+      else
+      {
+        Json::Value value = Json::objectValue;
+
+        value["Name"] = tagName;
+
+        if (it->second->IsNull())
+        {
+          value["Type"] = "Null";
+          value["Value"] = Json::nullValue;
+        }
+        else
+        {
+          // TODO IsBinary
+          value["Type"] = "String";
+          value["Value"] = it->second->GetContent();
+        }
+
+        result[it->first.Format()] = value;
+      }
+    }
+  }
+
+
+  std::string FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType level)
+  {
+    char uid[100];
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        // The "PatientID" field is of type LO (Long String), 64
+        // Bytes Maximum. An UUID is of length 36, thus it can be used
+        // as a random PatientID.
+        return Toolbox::GenerateUuid();
+
+      case ResourceType_Instance:
+        return dcmGenerateUniqueIdentifier(uid, SITE_INSTANCE_UID_ROOT);
+
+      case ResourceType_Series:
+        return dcmGenerateUniqueIdentifier(uid, SITE_SERIES_UID_ROOT);
+
+      case ResourceType_Study:
+        return dcmGenerateUniqueIdentifier(uid, SITE_STUDY_UID_ROOT);
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
+                                           DcmDataset& dataSet)
+  {
+    // Determine the transfer syntax which shall be used to write the
+    // information to the file. We always switch to the Little Endian
+    // syntax, with explicit length.
+
+    // http://support.dcmtk.org/docs/dcxfer_8h-source.html
+
+
+    /**
+     * Note that up to Orthanc 0.7.1 (inclusive), the
+     * "EXS_LittleEndianExplicit" was always used to save the DICOM
+     * dataset into memory. We now keep the original transfer syntax
+     * (if available).
+     **/
+    E_TransferSyntax xfer = dataSet.getOriginalXfer();
+    if (xfer == EXS_Unknown)
+    {
+      // No information about the original transfer syntax: This is
+      // most probably a DICOM dataset that was read from memory.
+      xfer = EXS_LittleEndianExplicit;
+    }
+
+    E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength;
+
+    // Create the meta-header information
+    DcmFileFormat ff(&dataSet);
+    ff.validateMetaInfo(xfer);
+    ff.removeInvalidGroups();
+
+    // Create a memory buffer with the proper size
+    {
+      const uint32_t estimatedSize = ff.calcElementLength(xfer, encodingType);  // (*)
+      buffer.resize(estimatedSize);
+    }
+
+    DcmOutputBufferStream ob(&buffer[0], buffer.size());
+
+    // Fill the memory buffer with the meta-header and the dataset
+    ff.transferInit();
+    OFCondition c = ff.write(ob, xfer, encodingType, NULL,
+                             /*opt_groupLength*/ EGL_recalcGL,
+                             /*opt_paddingType*/ EPD_withoutPadding);
+    ff.transferEnd();
+
+    if (c.good())
+    {
+      // 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 < buffer.size())
+      {
+        buffer.resize(effectiveSize);
+      }
+
+      return true;
+    }
+    else
+    {
+      // Error
+      buffer.clear();
+      return false;
+    }
+  }
+
+
+  ValueRepresentation FromDcmtkBridge::LookupValueRepresentation(const DicomTag& tag)
+  {
+    DcmTag t(tag.GetGroup(), tag.GetElement());
+    return Convert(t.getEVR());
+  }
+
+  ValueRepresentation FromDcmtkBridge::Convert(const DcmEVR vr)
+  {
+    switch (vr)
+    {
+      case EVR_AE:
+        return ValueRepresentation_ApplicationEntity;
+
+      case EVR_AS:
+        return ValueRepresentation_AgeString;
+
+      case EVR_AT:
+        return ValueRepresentation_AttributeTag;
+
+      case EVR_CS:
+        return ValueRepresentation_CodeString;
+
+      case EVR_DA:
+        return ValueRepresentation_Date;
+
+      case EVR_DS:
+        return ValueRepresentation_DecimalString;
+
+      case EVR_DT:
+        return ValueRepresentation_DateTime;
+
+      case EVR_FL:
+        return ValueRepresentation_FloatingPointSingle;
+
+      case EVR_FD:
+        return ValueRepresentation_FloatingPointDouble;
+
+      case EVR_IS:
+        return ValueRepresentation_IntegerString;
+
+      case EVR_LO:
+        return ValueRepresentation_LongString;
+
+      case EVR_LT:
+        return ValueRepresentation_LongText;
+
+      case EVR_OB:
+        return ValueRepresentation_OtherByte;
+
+        // Not supported as of DCMTK 3.6.0
+        /*case EVR_OD:
+          return ValueRepresentation_OtherDouble;*/
+
+      case EVR_OF:
+        return ValueRepresentation_OtherFloat;
+
+        // Not supported as of DCMTK 3.6.0
+        /*case EVR_OL:
+          return ValueRepresentation_OtherLong;*/
+
+      case EVR_OW:
+        return ValueRepresentation_OtherWord;
+
+      case EVR_PN:
+        return ValueRepresentation_PersonName;
+
+      case EVR_SH:
+        return ValueRepresentation_ShortString;
+
+      case EVR_SL:
+        return ValueRepresentation_SignedLong;
+
+      case EVR_SQ:
+        return ValueRepresentation_Sequence;
+
+      case EVR_SS:
+        return ValueRepresentation_SignedShort;
+
+      case EVR_ST:
+        return ValueRepresentation_ShortText;
+
+      case EVR_TM:
+        return ValueRepresentation_Time;
+
+        // Not supported as of DCMTK 3.6.0
+        /*case EVR_UC:
+          return ValueRepresentation_UnlimitedCharacters;*/
+
+      case EVR_UI:
+        return ValueRepresentation_UniqueIdentifier;
+
+      case EVR_UL:
+        return ValueRepresentation_UnsignedLong;
+
+      case EVR_UN:
+        return ValueRepresentation_Unknown;
+
+        // Not supported as of DCMTK 3.6.0
+        /*case EVR_UR:
+          return ValueRepresentation_UniversalResource;*/
+
+      case EVR_US:
+        return ValueRepresentation_UnsignedShort;
+
+      case EVR_UT:
+        return ValueRepresentation_UnlimitedText;
+
+      default:
+        return ValueRepresentation_NotSupported;
+    }
+  }
+
+
+  static bool IsBinaryTag(const DcmTag& key)
+  {
+    return (key.isUnknownVR() || 
+            key.getEVR() == EVR_OB ||
+            key.getEVR() == EVR_OF ||
+            key.getEVR() == EVR_OW ||
+            key.getEVR() == EVR_UN ||
+            key.getEVR() == EVR_ox);
+  }
+
+
+  DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag)
+  {
+    DcmTag key(tag.GetGroup(), tag.GetElement());
+
+    if (tag.IsPrivate() ||
+        IsBinaryTag(key))
+    {
+      return new DcmOtherByteOtherWord(key);
+    }
+
+    switch (key.getEVR())
+    {
+      // http://support.dcmtk.org/docs/dcvr_8h-source.html
+
+      /**
+       * Binary types, handled above
+       **/
+    
+      case EVR_OB:  // other byte
+      case EVR_OF:  // other float
+      case EVR_OW:  // other word
+      case EVR_UN:  // unknown value representation
+      case EVR_ox:  // OB or OW depending on context
+        throw OrthancException(ErrorCode_InternalError);
+
+
+      /**
+       * String types.
+       * http://support.dcmtk.org/docs/classDcmByteString.html
+       **/
+      
+      case EVR_AS:  // age string
+        return new DcmAgeString(key);
+
+      case EVR_AE:  // application entity title
+        return new DcmApplicationEntity(key);
+
+      case EVR_CS:  // code string
+        return new DcmCodeString(key);        
+
+      case EVR_DA:  // date string
+        return new DcmDate(key);
+        
+      case EVR_DT:  // date time string
+        return new DcmDateTime(key);
+
+      case EVR_DS:  // decimal string
+        return new DcmDecimalString(key);
+
+      case EVR_IS:  // integer string
+        return new DcmIntegerString(key);
+
+      case EVR_TM:  // time string
+        return new DcmTime(key);
+
+      case EVR_UI:  // unique identifier
+        return new DcmUniqueIdentifier(key);
+
+      case EVR_ST:  // short text
+        return new DcmShortText(key);
+
+      case EVR_LO:  // long string
+        return new DcmLongString(key);
+
+      case EVR_LT:  // long text
+        return new DcmLongText(key);
+
+      case EVR_UT:  // unlimited text
+        return new DcmUnlimitedText(key);
+
+      case EVR_SH:  // short string
+        return new DcmShortString(key);
+
+      case EVR_PN:  // person name
+        return new DcmPersonName(key);
+
+        
+      /**
+       * Numerical types
+       **/ 
+      
+      case EVR_SL:  // signed long
+        return new DcmSignedLong(key);
+
+      case EVR_SS:  // signed short
+        return new DcmSignedShort(key);
+
+      case EVR_UL:  // unsigned long
+        return new DcmUnsignedLong(key);
+
+      case EVR_US:  // unsigned short
+        return new DcmUnsignedShort(key);
+
+      case EVR_FL:  // float single-precision
+        return new DcmFloatingPointSingle(key);
+
+      case EVR_FD:  // float double-precision
+        return new DcmFloatingPointDouble(key);
+
+
+      /**
+       * Sequence types, should never occur at this point.
+       **/
+
+      case EVR_SQ:  // sequence of items
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+
+
+      /**
+       * TODO
+       **/
+
+      case EVR_AT:  // attribute tag
+        throw OrthancException(ErrorCode_NotImplemented);
+
+
+      /**
+       * Internal to DCMTK.
+       **/ 
+
+      case EVR_xs:  // SS or US depending on context
+      case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
+      case EVR_na:  // na="not applicable", for data which has no VR
+      case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
+      case EVR_item:  // used internally for items
+      case EVR_metainfo:  // used internally for meta info datasets
+      case EVR_dataset:  // used internally for datasets
+      case EVR_fileFormat:  // used internally for DICOM files
+      case EVR_dicomDir:  // used internally for DICOMDIR objects
+      case EVR_dirRecord:  // used internally for DICOMDIR records
+      case EVR_pixelSQ:  // used internally for pixel sequences in a compressed image
+      case EVR_pixelItem:  // used internally for pixel items in a compressed image
+      case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
+      case EVR_PixelData:  // used internally for uncompressed pixeld data
+      case EVR_OverlayData:  // used internally for overlay data
+      case EVR_UNKNOWN2B:  // used internally for elements with unknown VR with 2-byte length field in explicit VR
+      default:
+        break;
+    }
+
+    throw OrthancException(ErrorCode_InternalError);          
+  }
+
+
+
+  void FromDcmtkBridge::FillElementWithString(DcmElement& element,
+                                              const DicomTag& tag,
+                                              const std::string& utf8Value,
+                                              bool decodeDataUriScheme,
+                                              Encoding dicomEncoding)
+  {
+    std::string binary;
+    const std::string* decoded = &utf8Value;
+
+    if (decodeDataUriScheme &&
+        boost::starts_with(utf8Value, "data:application/octet-stream;base64,"))
+    {
+      std::string mime;
+      if (!Toolbox::DecodeDataUriScheme(mime, binary, utf8Value))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      decoded = &binary;
+    }
+    else if (dicomEncoding != Encoding_Utf8)
+    {
+      binary = Toolbox::ConvertFromUtf8(utf8Value, dicomEncoding);
+      decoded = &binary;
+    }
+
+    DcmTag key(tag.GetGroup(), tag.GetElement());
+
+    if (tag.IsPrivate() ||
+        IsBinaryTag(key))
+    {
+      if (element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good())
+      {
+        return;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    bool ok = false;
+    
+    try
+    {
+      switch (key.getEVR())
+      {
+        // http://support.dcmtk.org/docs/dcvr_8h-source.html
+
+        /**
+         * TODO.
+         **/
+
+        case EVR_OB:  // other byte
+        case EVR_OF:  // other float
+        case EVR_OW:  // other word
+        case EVR_AT:  // attribute tag
+          throw OrthancException(ErrorCode_NotImplemented);
+    
+        case EVR_UN:  // unknown value representation
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+
+
+        /**
+         * String types.
+         **/
+      
+        case EVR_DS:  // decimal string
+        case EVR_IS:  // integer string
+        case EVR_AS:  // age string
+        case EVR_DA:  // date string
+        case EVR_DT:  // date time string
+        case EVR_TM:  // time string
+        case EVR_AE:  // application entity title
+        case EVR_CS:  // code string
+        case EVR_SH:  // short string
+        case EVR_LO:  // long string
+        case EVR_ST:  // short text
+        case EVR_LT:  // long text
+        case EVR_UT:  // unlimited text
+        case EVR_PN:  // person name
+        case EVR_UI:  // unique identifier
+        {
+          ok = element.putString(decoded->c_str()).good();
+          break;
+        }
+
+        
+        /**
+         * Numerical types
+         **/ 
+      
+        case EVR_SL:  // signed long
+        {
+          ok = element.putSint32(boost::lexical_cast<Sint32>(*decoded)).good();
+          break;
+        }
+
+        case EVR_SS:  // signed short
+        {
+          ok = element.putSint16(boost::lexical_cast<Sint16>(*decoded)).good();
+          break;
+        }
+
+        case EVR_UL:  // unsigned long
+        {
+          ok = element.putUint32(boost::lexical_cast<Uint32>(*decoded)).good();
+          break;
+        }
+
+        case EVR_US:  // unsigned short
+        {
+          ok = element.putUint16(boost::lexical_cast<Uint16>(*decoded)).good();
+          break;
+        }
+
+        case EVR_FL:  // float single-precision
+        {
+          ok = element.putFloat32(boost::lexical_cast<float>(*decoded)).good();
+          break;
+        }
+
+        case EVR_FD:  // float double-precision
+        {
+          ok = element.putFloat64(boost::lexical_cast<double>(*decoded)).good();
+          break;
+        }
+
+
+        /**
+         * Sequence types, should never occur at this point.
+         **/
+
+        case EVR_SQ:  // sequence of items
+        {
+          ok = false;
+          break;
+        }
+
+
+        /**
+         * Internal to DCMTK.
+         **/ 
+
+        case EVR_ox:  // OB or OW depending on context
+        case EVR_xs:  // SS or US depending on context
+        case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
+        case EVR_na:  // na="not applicable", for data which has no VR
+        case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
+        case EVR_item:  // used internally for items
+        case EVR_metainfo:  // used internally for meta info datasets
+        case EVR_dataset:  // used internally for datasets
+        case EVR_fileFormat:  // used internally for DICOM files
+        case EVR_dicomDir:  // used internally for DICOMDIR objects
+        case EVR_dirRecord:  // used internally for DICOMDIR records
+        case EVR_pixelSQ:  // used internally for pixel sequences in a compressed image
+        case EVR_pixelItem:  // used internally for pixel items in a compressed image
+        case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
+        case EVR_PixelData:  // used internally for uncompressed pixeld data
+        case EVR_OverlayData:  // used internally for overlay data
+        case EVR_UNKNOWN2B:  // used internally for elements with unknown VR with 2-byte length field in explicit VR
+        default:
+          break;
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      ok = false;
+    }
+
+    if (!ok)
+    {
+      LOG(ERROR) << "While creating a DICOM instance, tag (" << tag.Format()
+                 << ") has out-of-range value: \"" << *decoded << "\"";
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  DcmElement* FromDcmtkBridge::FromJson(const DicomTag& tag,
+                                        const Json::Value& value,
+                                        bool decodeDataUriScheme,
+                                        Encoding dicomEncoding)
+  {
+    std::auto_ptr<DcmElement> element;
+
+    switch (value.type())
+    {
+      case Json::stringValue:
+        element.reset(CreateElementForTag(tag));
+        FillElementWithString(*element, tag, value.asString(), decodeDataUriScheme, dicomEncoding);
+        break;
+
+      case Json::nullValue:
+        element.reset(CreateElementForTag(tag));
+        FillElementWithString(*element, tag, "", decodeDataUriScheme, dicomEncoding);
+        break;
+
+      case Json::arrayValue:
+      {
+        DcmTag key(tag.GetGroup(), tag.GetElement());
+        if (key.getEVR() != EVR_SQ)
+        {
+          throw OrthancException(ErrorCode_BadParameterType);
+        }
+
+        DcmSequenceOfItems* sequence = new DcmSequenceOfItems(key);
+        element.reset(sequence);
+        
+        for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
+        {
+          std::auto_ptr<DcmItem> item(new DcmItem);
+
+          switch (value[i].type())
+          {
+            case Json::objectValue:
+            {
+              Json::Value::Members members = value[i].getMemberNames();
+              for (Json::Value::ArrayIndex j = 0; j < members.size(); j++)
+              {
+                item->insert(FromJson(ParseTag(members[j]), value[i][members[j]], decodeDataUriScheme, dicomEncoding));
+              }
+              break;
+            }
+
+            case Json::arrayValue:
+            {
+              // Lua cannot disambiguate between an empty dictionary
+              // and an empty array
+              if (value[i].size() != 0)
+              {
+                throw OrthancException(ErrorCode_BadParameterType);
+              }
+              break;
+            }
+
+            default:
+              throw OrthancException(ErrorCode_BadParameterType);
+          }
+
+          sequence->append(item.release());
+        }
+
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    return element.release();
+  }
+
+
+  DcmPixelSequence* FromDcmtkBridge::GetPixelSequence(DcmDataset& dataset)
+  {
+    DcmElement *element = NULL;
+    if (!dataset.findAndGetElement(DCM_PixelData, element).good())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    DcmPixelData& pixelData = dynamic_cast<DcmPixelData&>(*element);
+    DcmPixelSequence* pixelSequence = NULL;
+    if (!pixelData.getEncapsulatedRepresentation
+        (dataset.getOriginalXfer(), NULL, pixelSequence).good())
+    {
+      return NULL;
+    }
+    else
+    {
+      return pixelSequence;
+    }
+  }
+
+
+  Encoding FromDcmtkBridge::ExtractEncoding(const Json::Value& json,
+                                            Encoding defaultEncoding)
+  {
+    if (json.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    Encoding encoding = defaultEncoding;
+
+    const Json::Value::Members tags = json.getMemberNames();
+    
+    // Look for SpecificCharacterSet (0008,0005) in the JSON file
+    for (size_t i = 0; i < tags.size(); i++)
+    {
+      DicomTag tag = FromDcmtkBridge::ParseTag(tags[i]);
+      if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+        const Json::Value& value = json[tags[i]];
+        if (value.type() != Json::stringValue ||
+            (value.asString().length() != 0 &&
+             !GetDicomEncoding(encoding, value.asCString())))
+        {
+          LOG(ERROR) << "Unknown encoding while creating DICOM from JSON: " << value;
+          throw OrthancException(ErrorCode_BadRequest);
+        }
+
+        if (value.asString().length() == 0)
+        {
+          return defaultEncoding;
+        }
+      }
+    }
+
+    return encoding;
+  } 
+
+
+  static void SetString(DcmDataset& target,
+                        const DcmTag& tag,
+                        const std::string& value)
+  {
+    if (!target.putAndInsertString(tag, value.c_str()).good())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  DcmDataset* FromDcmtkBridge::FromJson(const Json::Value& json,  // Encoded using UTF-8
+                                        bool generateIdentifiers,
+                                        bool decodeDataUriScheme,
+                                        Encoding defaultEncoding)
+  {
+    std::auto_ptr<DcmDataset> result(new DcmDataset);
+    Encoding encoding = ExtractEncoding(json, defaultEncoding);
+
+    SetString(*result, DCM_SpecificCharacterSet, GetDicomSpecificCharacterSet(encoding));
+
+    const Json::Value::Members tags = json.getMemberNames();
+    
+    bool hasPatientId = false;
+    bool hasStudyInstanceUid = false;
+    bool hasSeriesInstanceUid = false;
+    bool hasSopInstanceUid = false;
+
+    for (size_t i = 0; i < tags.size(); i++)
+    {
+      DicomTag tag = FromDcmtkBridge::ParseTag(tags[i]);
+      const Json::Value& value = json[tags[i]];
+
+      if (tag == DICOM_TAG_PATIENT_ID)
+      {
+        hasPatientId = true;
+      }
+      else if (tag == DICOM_TAG_STUDY_INSTANCE_UID)
+      {
+        hasStudyInstanceUid = true;
+      }
+      else if (tag == DICOM_TAG_SERIES_INSTANCE_UID)
+      {
+        hasSeriesInstanceUid = true;
+      }
+      else if (tag == DICOM_TAG_SOP_INSTANCE_UID)
+      {
+        hasSopInstanceUid = true;
+      }
+
+      if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+        std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding));
+        const DcmTagKey& tag = element->getTag();
+
+        result->findAndDeleteElement(tag);
+
+        DcmElement* tmp = element.release();
+        if (!result->insert(tmp, false, false).good())
+        {
+          delete tmp;
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    }
+
+    if (!hasPatientId &&
+        generateIdentifiers)
+    {
+      SetString(*result, DCM_PatientID, GenerateUniqueIdentifier(ResourceType_Patient));
+    }
+
+    if (!hasStudyInstanceUid &&
+        generateIdentifiers)
+    {
+      SetString(*result, DCM_StudyInstanceUID, GenerateUniqueIdentifier(ResourceType_Study));
+    }
+
+    if (!hasSeriesInstanceUid &&
+        generateIdentifiers)
+    {
+      SetString(*result, DCM_SeriesInstanceUID, GenerateUniqueIdentifier(ResourceType_Series));
+    }
+
+    if (!hasSopInstanceUid &&
+        generateIdentifiers)
+    {
+      SetString(*result, DCM_SOPInstanceUID, GenerateUniqueIdentifier(ResourceType_Instance));
+    }
+
+    return result.release();
+  }
+
+
+  DcmFileFormat* FromDcmtkBridge::LoadFromMemoryBuffer(const void* buffer,
+                                                       size_t size)
+  {
+    DcmInputBufferStream is;
+    if (size > 0)
+    {
+      is.setBuffer(buffer, size);
+    }
+    is.setEos();
+
+    std::auto_ptr<DcmFileFormat> result(new DcmFileFormat);
+
+    result->transferInit();
+    if (!result->read(is).good())
+    {
+      LOG(ERROR) << "Cannot parse an invalid DICOM file (size: " << size << " bytes)";
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    result->loadAllDataIntoMemory();
+    result->transferEnd();
+
+    return result.release();
+  }
+
+
+  void FromDcmtkBridge::FromJson(DicomMap& target,
+                                 const Json::Value& source)
+  {
+    if (source.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    target.Clear();
+
+    Json::Value::Members members = source.getMemberNames();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const Json::Value& value = source[members[i]];
+
+      if (value.type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      
+      target.SetValue(ParseTag(members[i]), value.asString(), false);
+    }
+  }
+
+
+  void FromDcmtkBridge::ChangeStringEncoding(DcmItem& dataset,
+                                             Encoding source,
+                                             Encoding target)
+  {
+    // Recursive exploration of a dataset to change the encoding of
+    // each string-like element
+
+    if (source == target)
+    {
+      return;
+    }
+
+    for (unsigned long i = 0; i < dataset.card(); i++)
+    {
+      DcmElement* element = dataset.getElement(i);
+      if (element)
+      {
+        if (element->isLeaf())
+        {
+          char *c = NULL;
+          if (element->isaString() &&
+              element->getString(c).good() && 
+              c != NULL)
+          {
+            std::string a = Toolbox::ConvertToUtf8(c, source);
+            std::string b = Toolbox::ConvertFromUtf8(a, target);
+            element->putString(b.c_str());
+          }
+        }
+        else
+        {
+          // "All subclasses of DcmElement except for DcmSequenceOfItems
+          // are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset
+          // etc. are not." The following dynamic_cast is thus OK.
+          DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(*element);
+
+          for (unsigned long j = 0; j < sequence.card(); j++)
+          {
+            ChangeStringEncoding(*sequence.getItem(j), source, target);
+          }
+        }
+      }
+    }
+  }
+
+
+  bool FromDcmtkBridge::LookupTransferSyntax(std::string& result,
+                                             DcmFileFormat& dicom)
+  {
+    const char* value = NULL;
+
+    if (dicom.getMetaInfo() != NULL &&
+        dicom.getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() &&
+        value != NULL)
+    {
+      result.assign(value);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+#if ORTHANC_ENABLE_LUA == 1
+  void FromDcmtkBridge::ExecuteToDicom(DicomMap& target,
+                                       LuaFunctionCall& call)
+  {
+    Json::Value output;
+    call.ExecuteToJson(output, true /* keep strings */);
+
+    target.Clear();
+
+    if (output.type() == Json::arrayValue &&
+        output.size() == 0)
+    {
+      // This case happens for empty tables
+      return;
+    }
+
+    if (output.type() != Json::objectValue)
+    {
+      LOG(ERROR) << "Lua: IncomingFindRequestFilter must return a table";
+      throw OrthancException(ErrorCode_LuaBadOutput);
+    }
+
+    Json::Value::Members members = output.getMemberNames();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      if (output[members[i]].type() != Json::stringValue)
+      {
+        LOG(ERROR) << "Lua: IncomingFindRequestFilter must return a table mapping names of DICOM tags to strings";
+        throw OrthancException(ErrorCode_LuaBadOutput);
+      }
+
+      DicomTag tag(ParseTag(members[i]));
+      target.SetValue(tag, output[members[i]].asString(), false);
+    }
+  }
+#endif
+
+
+  void FromDcmtkBridge::ExtractDicomSummary(DicomMap& target, 
+                                            DcmItem& dataset)
+  {
+    ExtractDicomSummary(target, dataset,
+                        ORTHANC_MAXIMUM_TAG_LENGTH,
+                        GetDefaultDicomEncoding());
+  }
+
+  
+  void FromDcmtkBridge::ExtractDicomAsJson(Json::Value& target, 
+                                           DcmDataset& dataset,
+                                           const std::set<DicomTag>& ignoreTagLength)
+  {
+    ExtractDicomAsJson(target, dataset, 
+                       DicomToJsonFormat_Full,
+                       DicomToJsonFlags_Default, 
+                       ORTHANC_MAXIMUM_TAG_LENGTH,
+                       GetDefaultDicomEncoding(),
+                       ignoreTagLength);
+  }
+
+
+  void FromDcmtkBridge::InitializeCodecs()
+  {
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+    LOG(INFO) << "Registering JPEG Lossless codecs in DCMTK";
+    DJLSDecoderRegistration::registerCodecs();    
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    LOG(INFO) << "Registering JPEG codecs in DCMTK";
+    DJDecoderRegistration::registerCodecs(); 
+#endif
+  }
+
+
+  void FromDcmtkBridge::FinalizeCodecs()
+  {
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+    // Unregister JPEG-LS codecs
+    DJLSDecoderRegistration::cleanup();
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    // Unregister JPEG codecs
+    DJDecoderRegistration::cleanup();
+#endif
+  }
+
+
+
+  // Forward declaration
+  static void ApplyVisitorToElement(DcmElement& element,
+                                    ITagVisitor& visitor,
+                                    const std::vector<DicomTag>& parentTags,
+                                    const std::vector<size_t>& parentIndexes,
+                                    Encoding encoding);
+ 
+  static void ApplyVisitorToDataset(DcmItem& dataset,
+                                    ITagVisitor& visitor,
+                                    const std::vector<DicomTag>& parentTags,
+                                    const std::vector<size_t>& parentIndexes,
+                                    Encoding encoding)
+  {
+    assert(parentTags.size() == parentIndexes.size());
+
+    for (unsigned long i = 0; i < dataset.card(); i++)
+    {
+      DcmElement* element = dataset.getElement(i);
+      if (element == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      else
+      {
+        ApplyVisitorToElement(*element, visitor, parentTags, parentIndexes, encoding);
+      }      
+    }
+  }
+
+
+  static void ApplyVisitorToLeaf(DcmElement& element,
+                                 ITagVisitor& visitor,
+                                 const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 Encoding encoding)
+  {
+    // TODO - Merge this function with ConvertLeafElement()
+
+    assert(element.isLeaf());
+
+    DcmEVR evr = element.getTag().getEVR();
+    ValueRepresentation vr = FromDcmtkBridge::Convert(evr);
+
+    char *c = NULL;
+    if (element.isaString() &&
+        element.getString(c).good())
+    {
+      std::string utf8;
+
+      if (c != NULL)  // This case corresponds to the empty string
+      {
+        std::string s(c);
+        utf8 = Toolbox::ConvertToUtf8(s, encoding);
+      }
+
+      std::string newValue;
+      ITagVisitor::Action action = visitor.VisitString
+        (newValue, parentTags, parentIndexes, tag, vr, utf8);
+
+      switch (action)
+      {
+        case ITagVisitor::Action_None:
+          break;
+
+        case ITagVisitor::Action_Replace:
+        {
+          std::string s = Toolbox::ConvertFromUtf8(newValue, encoding);
+          if (element.putString(s.c_str()) != EC_Normal)
+          {
+            LOG(ERROR) << "Cannot replace value of tag: " << tag.Format();
+            throw OrthancException(ErrorCode_InternalError);
+          }
+
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      return;  // We're done
+    }
+
+
+    try
+    {
+      // http://support.dcmtk.org/docs/dcvr_8h-source.html
+      switch (element.getVR())
+      {
+
+        /**
+         * Deal with binary data (including PixelData).
+         **/
+
+        case EVR_OB:  // other byte
+        case EVR_OF:  // other float
+        case EVR_OW:  // other word
+        case EVR_UN:  // unknown value representation
+        case EVR_ox:  // OB or OW depending on context
+        case EVR_DS:  // decimal string
+        case EVR_IS:  // integer string
+        case EVR_AS:  // age string
+        case EVR_DA:  // date string
+        case EVR_DT:  // date time string
+        case EVR_TM:  // time string
+        case EVR_AE:  // application entity title
+        case EVR_CS:  // code string
+        case EVR_SH:  // short string
+        case EVR_LO:  // long string
+        case EVR_ST:  // short text
+        case EVR_LT:  // long text
+        case EVR_UT:  // unlimited text
+        case EVR_PN:  // person name
+        case EVR_UI:  // unique identifier
+        case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
+        case EVR_UNKNOWN2B:  // used internally for elements with unknown VR with 2-byte length field in explicit VR
+        {
+          Uint8* data = NULL;
+
+          if (element.getUint8Array(data) == EC_Normal)
+          {
+            visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength());
+          }
+          else
+          {
+            visitor.VisitUnknown(parentTags, parentIndexes, tag, vr);
+          }
+
+          break;
+        }
+    
+        /**
+         * Numeric types
+         **/ 
+      
+        case EVR_SL:  // signed long
+        {
+          Sint32 f;
+          if (dynamic_cast<DcmSignedLong&>(element).getSint32(f).good())
+          {
+            visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f);
+          }
+
+          break;
+        }
+
+        case EVR_SS:  // signed short
+        {
+          Sint16 f;
+          if (dynamic_cast<DcmSignedShort&>(element).getSint16(f).good())
+          {
+            visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f);
+          }
+
+          break;
+        }
+
+        case EVR_UL:  // unsigned long
+        {
+          Uint32 f;
+          if (dynamic_cast<DcmUnsignedLong&>(element).getUint32(f).good())
+          {
+            visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f);
+          }
+
+          break;
+        }
+
+        case EVR_US:  // unsigned short
+        {
+          Uint16 f;
+          if (dynamic_cast<DcmUnsignedShort&>(element).getUint16(f).good())
+          {
+            visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f);
+          }
+
+          break;
+        }
+
+        case EVR_FL:  // float single-precision
+        {
+          Float32 f;
+          if (dynamic_cast<DcmFloatingPointSingle&>(element).getFloat32(f).good())
+          {
+            visitor.VisitDouble(parentTags, parentIndexes, tag, vr, f);
+          }
+
+          break;
+        }
+
+        case EVR_FD:  // float double-precision
+        {
+          Float64 f;
+          if (dynamic_cast<DcmFloatingPointDouble&>(element).getFloat64(f).good())
+          {
+            visitor.VisitDouble(parentTags, parentIndexes, tag, vr, f);
+          }
+
+          break;
+        }
+
+
+        /**
+         * Attribute tag.
+         **/
+
+        case EVR_AT:
+        {
+          DcmTagKey tagKey;
+          if (dynamic_cast<DcmAttributeTag&>(element).getTagVal(tagKey, 0).good())
+          {
+            DicomTag t(tagKey.getGroup(), tagKey.getElement());
+            visitor.VisitAttribute(parentTags, parentIndexes, tag, vr, t);
+          }
+
+          break;
+        }
+
+
+        /**
+         * Sequence types, should never occur at this point because of
+         * "element.isLeaf()".
+         **/
+
+        case EVR_SQ:  // sequence of items
+          return;
+
+
+          /**
+           * Internal to DCMTK.
+           **/ 
+
+        case EVR_xs:  // SS or US depending on context
+        case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
+        case EVR_na:  // na="not applicable", for data which has no VR
+        case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
+        case EVR_item:  // used internally for items
+        case EVR_metainfo:  // used internally for meta info datasets
+        case EVR_dataset:  // used internally for datasets
+        case EVR_fileFormat:  // used internally for DICOM files
+        case EVR_dicomDir:  // used internally for DICOMDIR objects
+        case EVR_dirRecord:  // used internally for DICOMDIR records
+        case EVR_pixelSQ:  // used internally for pixel sequences in a compressed image
+        case EVR_pixelItem:  // used internally for pixel items in a compressed image
+        case EVR_PixelData:  // used internally for uncompressed pixeld data
+        case EVR_OverlayData:  // used internally for overlay data
+          visitor.VisitUnknown(parentTags, parentIndexes, tag, vr);
+          return;
+
+
+          /**
+           * Default case.
+           **/ 
+
+        default:
+          return;
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      return;
+    }
+    catch (std::bad_cast&)
+    {
+      return;
+    }
+  }
+
+
+  static void ApplyVisitorToElement(DcmElement& element,
+                                    ITagVisitor& visitor,
+                                    const std::vector<DicomTag>& parentTags,
+                                    const std::vector<size_t>& parentIndexes,
+                                    Encoding encoding)
+  {
+    assert(parentTags.size() == parentIndexes.size());
+
+    DicomTag tag(FromDcmtkBridge::Convert(element.getTag()));
+
+    if (element.isLeaf())
+    {
+      ApplyVisitorToLeaf(element, visitor, parentTags, parentIndexes, tag, encoding);
+    }
+    else
+    {
+      // "All subclasses of DcmElement except for DcmSequenceOfItems
+      // are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset
+      // etc. are not." The following dynamic_cast is thus OK.
+      DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(element);
+
+      std::vector<DicomTag> tags = parentTags;
+      std::vector<size_t> indexes = parentIndexes;
+      tags.push_back(tag);
+      indexes.push_back(0);
+
+      for (unsigned long i = 0; i < sequence.card(); i++)
+      {
+        indexes.back() = static_cast<size_t>(i);
+        DcmItem* child = sequence.getItem(i);
+        ApplyVisitorToDataset(*child, visitor, tags, indexes, encoding);
+      }
+    }
+  }
+
+
+  void FromDcmtkBridge::Apply(DcmItem& dataset,
+                              ITagVisitor& visitor,
+                              Encoding defaultEncoding)
+  {
+    std::vector<DicomTag> parentTags;
+    std::vector<size_t> parentIndexes;
+    Encoding encoding = DetectEncoding(dataset, defaultEncoding);
+    ApplyVisitorToDataset(dataset, visitor, parentTags, parentIndexes, encoding);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/FromDcmtkBridge.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,249 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ITagVisitor.h"
+#include "../DicomFormat/DicomElement.h"
+#include "../DicomFormat/DicomMap.h"
+
+#include <dcmtk/dcmdata/dcdatset.h>
+#include <dcmtk/dcmdata/dcmetinf.h>
+#include <dcmtk/dcmdata/dcpixseq.h>
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <json/json.h>
+
+#if !defined(ORTHANC_ENABLE_LUA)
+#  error The macro ORTHANC_ENABLE_LUA must be defined
+#endif
+
+#if ORTHANC_ENABLE_DCMTK != 1
+#  error The macro ORTHANC_ENABLE_DCMTK must be set to 1
+#endif
+
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+#  include <gtest/gtest_prod.h>
+#endif
+
+#if ORTHANC_ENABLE_LUA == 1
+#  include "../Lua/LuaFunctionCall.h"
+#endif
+
+#if !defined(ORTHANC_ENABLE_DCMTK_JPEG)
+#  error The macro ORTHANC_ENABLE_DCMTK_JPEG must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS)
+#  error The macro ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS must be defined
+#endif
+
+
+namespace Orthanc
+{
+  class FromDcmtkBridge : public boost::noncopyable
+  {
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+    FRIEND_TEST(FromDcmtkBridge, FromJson);
+#endif
+
+    friend class ParsedDicomFile;
+
+  private:
+    FromDcmtkBridge();  // Pure static class
+
+    static void ExtractDicomSummary(DicomMap& target, 
+                                    DcmItem& dataset,
+                                    unsigned int maxStringLength,
+                                    Encoding defaultEncoding);
+
+    static void DatasetToJson(Json::Value& parent,
+                              DcmItem& item,
+                              DicomToJsonFormat format,
+                              DicomToJsonFlags flags,
+                              unsigned int maxStringLength,
+                              Encoding encoding,
+                              const std::set<DicomTag>& ignoreTagLength);
+
+    static void ElementToJson(Json::Value& parent,
+                              DcmElement& element,
+                              DicomToJsonFormat format,
+                              DicomToJsonFlags flags,
+                              unsigned int maxStringLength,
+                              Encoding dicomEncoding,
+                              const std::set<DicomTag>& ignoreTagLength);
+
+    static void ExtractDicomAsJson(Json::Value& target, 
+                                   DcmDataset& dataset,
+                                   DicomToJsonFormat format,
+                                   DicomToJsonFlags flags,
+                                   unsigned int maxStringLength,
+                                   Encoding defaultEncoding,
+                                   const std::set<DicomTag>& ignoreTagLength);
+
+    static void ChangeStringEncoding(DcmItem& dataset,
+                                     Encoding source,
+                                     Encoding target);
+
+  public:
+    static void InitializeDictionary(bool loadPrivateDictionary);
+
+    static void RegisterDictionaryTag(const DicomTag& tag,
+                                      ValueRepresentation vr,
+                                      const std::string& name,
+                                      unsigned int minMultiplicity,
+                                      unsigned int maxMultiplicity,
+                                      const std::string& privateCreator);
+
+    static Encoding DetectEncoding(DcmItem& dataset,
+                                   Encoding defaultEncoding);
+
+    static DicomTag Convert(const DcmTag& tag);
+
+    static DicomTag GetTag(const DcmElement& element);
+
+    static bool IsUnknownTag(const DicomTag& tag);
+
+    static DicomValue* ConvertLeafElement(DcmElement& element,
+                                          DicomToJsonFlags flags,
+                                          unsigned int maxStringLength,
+                                          Encoding encoding,
+                                          const std::set<DicomTag>& ignoreTagLength);
+
+    static void ExtractHeaderAsJson(Json::Value& target, 
+                                    DcmMetaInfo& header,
+                                    DicomToJsonFormat format,
+                                    DicomToJsonFlags flags,
+                                    unsigned int maxStringLength);
+
+    static std::string GetTagName(const DicomTag& tag,
+                                  const std::string& privateCreator);
+
+    static std::string GetTagName(const DcmElement& element);
+
+    static std::string GetTagName(const DicomElement& element)
+    {
+      return GetTagName(element.GetTag(), "");
+    }
+
+    static DicomTag ParseTag(const char* name);
+
+    static DicomTag ParseTag(const std::string& name)
+    {
+      return ParseTag(name.c_str());
+    }
+
+    static bool HasTag(const DicomMap& fields,
+                       const std::string& tagName)
+    {
+      return fields.HasTag(ParseTag(tagName));
+    }
+
+    static const DicomValue& GetValue(const DicomMap& fields,
+                                      const std::string& tagName)
+    {
+      return fields.GetValue(ParseTag(tagName));
+    }
+
+    static void SetValue(DicomMap& target,
+                         const std::string& tagName,
+                         DicomValue* value)
+    {
+      target.SetValue(ParseTag(tagName), value);
+    }
+
+    static void ToJson(Json::Value& result,
+                       const DicomMap& values,
+                       bool simplify);
+
+    static std::string GenerateUniqueIdentifier(ResourceType level);
+
+    static bool SaveToMemoryBuffer(std::string& buffer,
+                                   DcmDataset& dataSet);
+
+    static ValueRepresentation Convert(DcmEVR vr);
+
+    static ValueRepresentation LookupValueRepresentation(const DicomTag& tag);
+
+    static DcmElement* CreateElementForTag(const DicomTag& tag);
+    
+    static void FillElementWithString(DcmElement& element,
+                                      const DicomTag& tag,
+                                      const std::string& utf8alue,  // Encoded using UTF-8
+                                      bool decodeDataUriScheme,
+                                      Encoding dicomEncoding);
+
+    static DcmElement* FromJson(const DicomTag& tag,
+                                const Json::Value& element,  // Encoded using UTF-8
+                                bool decodeDataUriScheme,
+                                Encoding dicomEncoding);
+
+    static DcmPixelSequence* GetPixelSequence(DcmDataset& dataset);
+
+    static Encoding ExtractEncoding(const Json::Value& json,
+                                    Encoding defaultEncoding);
+
+    static DcmDataset* FromJson(const Json::Value& json,  // Encoded using UTF-8
+                                bool generateIdentifiers,
+                                bool decodeDataUriScheme,
+                                Encoding defaultEncoding);
+
+    static DcmFileFormat* LoadFromMemoryBuffer(const void* buffer,
+                                               size_t size);
+
+    static void FromJson(DicomMap& values,
+                         const Json::Value& result);
+
+    static bool LookupTransferSyntax(std::string& result,
+                                     DcmFileFormat& dicom);
+
+#if ORTHANC_ENABLE_LUA == 1
+    static void ExecuteToDicom(DicomMap& target,
+                               LuaFunctionCall& call);
+#endif
+
+    static void ExtractDicomSummary(DicomMap& target, 
+                                    DcmItem& dataset);
+
+    static void ExtractDicomAsJson(Json::Value& target, 
+                                   DcmDataset& dataset,
+                                   const std::set<DicomTag>& ignoreTagLength);
+
+    static void InitializeCodecs();
+
+    static void FinalizeCodecs();
+
+    static void Apply(DcmItem& dataset,
+                      ITagVisitor& visitor,
+                      Encoding defaultEncoding);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/ITagVisitor.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../DicomFormat/DicomTag.h"
+
+#include <vector>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class ITagVisitor : public boost::noncopyable
+  {
+  public:
+    enum Action
+    {
+      Action_Replace,
+      Action_None
+    };
+
+    virtual ~ITagVisitor()
+    {
+    }
+
+    virtual void VisitUnknown(const std::vector<DicomTag>& parentTags,
+                              const std::vector<size_t>& parentIndexes,
+                              const DicomTag& tag,
+                              ValueRepresentation vr) = 0;
+
+    virtual void VisitBinary(const std::vector<DicomTag>& parentTags,
+                             const std::vector<size_t>& parentIndexes,
+                             const DicomTag& tag,
+                             ValueRepresentation vr,
+                             const void* data,
+                             size_t size) = 0;
+
+    virtual void VisitInteger(const std::vector<DicomTag>& parentTags,
+                              const std::vector<size_t>& parentIndexes,
+                              const DicomTag& tag,
+                              ValueRepresentation vr,
+                              int64_t value) = 0;
+
+    virtual void VisitDouble(const std::vector<DicomTag>& parentTags,
+                             const std::vector<size_t>& parentIndexes,
+                             const DicomTag& tag,
+                             ValueRepresentation vr,
+                             double value) = 0;
+
+    virtual void VisitAttribute(const std::vector<DicomTag>& parentTags,
+                                const std::vector<size_t>& parentIndexes,
+                                const DicomTag& tag,
+                                ValueRepresentation vr,
+                                const DicomTag& value) = 0;
+
+    virtual Action VisitString(std::string& newValue,
+                               const std::vector<DicomTag>& parentTags,
+                               const std::vector<size_t>& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const std::string& value) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/Internals/DicomFrameIndex.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,439 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "DicomFrameIndex.h"
+
+#include "../../OrthancException.h"
+#include "../../DicomFormat/DicomImageInformation.h"
+#include "../FromDcmtkBridge.h"
+#include "../../Endianness.h"
+#include "DicomImageDecoder.h"
+
+#include <boost/lexical_cast.hpp>
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcpxitem.h>
+#include <dcmtk/dcmdata/dcpixseq.h>
+
+namespace Orthanc
+{
+  class DicomFrameIndex::FragmentIndex : public DicomFrameIndex::IIndex
+  {
+  private:
+    DcmPixelSequence*           pixelSequence_;
+    std::vector<DcmPixelItem*>  startFragment_;
+    std::vector<unsigned int>   countFragments_;
+    std::vector<unsigned int>   frameSize_;
+
+    void GetOffsetTable(std::vector<uint32_t>& table)
+    {
+      DcmPixelItem* item = NULL;
+      if (!pixelSequence_->getItem(item, 0).good() ||
+          item == NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      uint32_t length = item->getLength();
+      if (length == 0)
+      {
+        table.clear();
+        return;
+      }
+
+      if (length % 4 != 0)
+      {
+        // Error: Each fragment is index with 4 bytes (uint32_t)
+        throw OrthancException(ErrorCode_BadFileFormat);        
+      }
+
+      uint8_t* content = NULL;
+      if (!item->getUint8Array(content).good() ||
+          content == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      table.resize(length / 4);
+
+      // The offset table is always in little endian in the DICOM
+      // file. Swap it to host endianness if needed.
+      const uint32_t* offset = reinterpret_cast<const uint32_t*>(content);
+      for (size_t i = 0; i < table.size(); i++, offset++)
+      {
+        table[i] = le32toh(*offset);
+      }
+    }
+
+
+  public:
+    FragmentIndex(DcmPixelSequence* pixelSequence,
+                  unsigned int countFrames) :
+      pixelSequence_(pixelSequence)
+    {
+      assert(pixelSequence != NULL);
+
+      startFragment_.resize(countFrames);
+      countFragments_.resize(countFrames);
+      frameSize_.resize(countFrames);
+
+      // The first fragment corresponds to the offset table
+      unsigned int countFragments = static_cast<unsigned int>(pixelSequence_->card());
+      if (countFragments < countFrames + 1)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      if (countFragments == countFrames + 1)
+      {
+        // Simple case: There is one fragment per frame.
+
+        DcmObject* fragment = pixelSequence_->nextInContainer(NULL);  // Skip the offset table
+        if (fragment == NULL)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        for (unsigned int i = 0; i < countFrames; i++)
+        {
+          fragment = pixelSequence_->nextInContainer(fragment);
+          startFragment_[i] = dynamic_cast<DcmPixelItem*>(fragment);
+          frameSize_[i] = fragment->getLength();
+          countFragments_[i] = 1;
+        }
+
+        return;
+      }
+
+      // Parse the offset table
+      std::vector<uint32_t> offsetOfFrame;
+      GetOffsetTable(offsetOfFrame);
+      
+      if (offsetOfFrame.size() != countFrames ||
+          offsetOfFrame[0] != 0)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+
+      // Loop over the fragments (ignoring the offset table). This is
+      // an alternative, faster implementation to DCMTK's
+      // "DcmCodec::determineStartFragment()".
+      DcmObject* fragment = pixelSequence_->nextInContainer(NULL);
+      if (fragment == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      fragment = pixelSequence_->nextInContainer(fragment); // Skip the offset table
+      if (fragment == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      uint32_t offset = 0;
+      unsigned int currentFrame = 0;
+      startFragment_[0] = dynamic_cast<DcmPixelItem*>(fragment);
+
+      unsigned int currentFragment = 1;
+      while (fragment != NULL)
+      {
+        if (currentFrame + 1 < countFrames &&
+            offset == offsetOfFrame[currentFrame + 1])
+        {
+          currentFrame += 1;
+          startFragment_[currentFrame] = dynamic_cast<DcmPixelItem*>(fragment);
+        }
+
+        frameSize_[currentFrame] += fragment->getLength();
+        countFragments_[currentFrame]++;
+
+        // 8 bytes = overhead for the item tag and length field
+        offset += fragment->getLength() + 8;
+
+        currentFragment++;
+        fragment = pixelSequence_->nextInContainer(fragment);
+      }
+
+      if (currentFragment != countFragments ||
+          currentFrame + 1 != countFrames ||
+          fragment != NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      assert(startFragment_.size() == countFragments_.size() &&
+             startFragment_.size() == frameSize_.size());
+    }
+
+
+    virtual void GetRawFrame(std::string& frame,
+                             unsigned int index) const
+    {
+      if (index >= startFragment_.size())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      frame.resize(frameSize_[index]);
+      if (frame.size() == 0)
+      {
+        return;
+      }
+
+      uint8_t* target = reinterpret_cast<uint8_t*>(&frame[0]);
+
+      size_t offset = 0;
+      DcmPixelItem* fragment = startFragment_[index];
+      for (unsigned int i = 0; i < countFragments_[index]; i++)
+      {
+        uint8_t* content = NULL;
+        if (!fragment->getUint8Array(content).good() ||
+            content == NULL)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        assert(offset + fragment->getLength() <= frame.size());
+
+        memcpy(target + offset, content, fragment->getLength());
+        offset += fragment->getLength();
+
+        fragment = dynamic_cast<DcmPixelItem*>(pixelSequence_->nextInContainer(fragment));
+      }
+    }
+  };
+
+
+
+  class DicomFrameIndex::UncompressedIndex : public DicomFrameIndex::IIndex
+  {
+  private:
+    uint8_t*  pixelData_;
+    size_t    frameSize_;
+
+  public: 
+    UncompressedIndex(DcmDataset& dataset,
+                      unsigned int countFrames,
+                      size_t frameSize) :
+      pixelData_(NULL),
+      frameSize_(frameSize)
+    {
+      size_t size = 0;
+
+      DcmElement* e;
+      if (dataset.findAndGetElement(DCM_PixelData, e).good() &&
+          e != NULL)
+      {
+        size = e->getLength();
+
+        if (size > 0)
+        {
+          pixelData_ = NULL;
+          if (!e->getUint8Array(pixelData_).good() ||
+              pixelData_ == NULL)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+        }
+      }
+
+      if (size < frameSize_ * countFrames)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+
+    virtual void GetRawFrame(std::string& frame,
+                             unsigned int index) const
+    {
+      frame.resize(frameSize_);
+      if (frameSize_ > 0)
+      {
+        memcpy(&frame[0], pixelData_ + index * frameSize_, frameSize_);
+      }
+    }
+  };
+
+
+  class DicomFrameIndex::PsmctRle1Index : public DicomFrameIndex::IIndex
+  {
+  private:
+    std::string  pixelData_;
+    size_t       frameSize_;
+
+  public: 
+    PsmctRle1Index(DcmDataset& dataset,
+                   unsigned int countFrames,
+                   size_t frameSize) :
+      frameSize_(frameSize)
+    {
+      if (!DicomImageDecoder::DecodePsmctRle1(pixelData_, dataset) ||
+          pixelData_.size() < frameSize * countFrames)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+
+    virtual void GetRawFrame(std::string& frame,
+                             unsigned int index) const
+    {
+      frame.resize(frameSize_);
+      if (frameSize_ > 0)
+      {
+        memcpy(&frame[0], reinterpret_cast<const uint8_t*>(&pixelData_[0]) + index * frameSize_, frameSize_);
+      }
+    }
+  };
+
+
+
+  bool DicomFrameIndex::IsVideo(DcmFileFormat& dicom)
+  {
+    // Retrieve the transfer syntax from the DICOM header
+    const char* value = NULL;
+    if (!dicom.getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() ||
+        value == NULL)
+    {
+      return false;
+    }
+
+    const std::string transferSyntax(value);
+
+    // Video standards supported in DICOM 2016a
+    // http://dicom.nema.org/medical/dicom/2016a/output/html/part05.html
+    if (transferSyntax == "1.2.840.10008.1.2.4.100" ||  // MPEG2 MP@ML option of ISO/IEC MPEG2
+        transferSyntax == "1.2.840.10008.1.2.4.101" ||  // MPEG2 MP@HL option of ISO/IEC MPEG2
+        transferSyntax == "1.2.840.10008.1.2.4.102" ||  // MPEG-4 AVC/H.264 High Profile / Level 4.1 of ITU-T H.264
+        transferSyntax == "1.2.840.10008.1.2.4.103" ||  // MPEG-4 AVC/H.264 BD-compat High Profile / Level 4.1 of ITU-T H.264
+        transferSyntax == "1.2.840.10008.1.2.4.104" ||  // MPEG-4 AVC/H.264 High Profile / Level 4.2 of ITU-T H.264
+        transferSyntax == "1.2.840.10008.1.2.4.105" ||  // MPEG-4 AVC/H.264 High Profile / Level 4.2 of ITU-T H.264
+        transferSyntax == "1.2.840.10008.1.2.4.106")    // MPEG-4 AVC/H.264 Stereo High Profile / Level 4.2 of the ITU-T H.264
+    {
+      return true;
+    }
+
+    return false;
+  }
+
+
+  unsigned int DicomFrameIndex::GetFramesCount(DcmFileFormat& dicom)
+  {
+    // Assume 1 frame for video transfer syntaxes
+    if (IsVideo(dicom))
+    {
+      return 1;
+    }        
+
+    const char* tmp = NULL;
+    if (!dicom.getDataset()->findAndGetString(DCM_NumberOfFrames, tmp).good() ||
+        tmp == NULL)
+    {
+      return 1;
+    }
+
+    int count = -1;
+    try
+    {
+      count = boost::lexical_cast<int>(tmp);
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+    }
+
+    if (count < 0)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);        
+    }
+    else
+    {
+      return count;
+    }
+  }
+
+
+  DicomFrameIndex::DicomFrameIndex(DcmFileFormat& dicom)
+  {
+    countFrames_ = GetFramesCount(dicom);
+    if (countFrames_ == 0)
+    {
+      // The image has no frame. No index is to be built.
+      return;
+    }
+
+    DcmDataset& dataset = *dicom.getDataset();
+
+    // Test whether this image is composed of a sequence of fragments
+    DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dataset);
+    if (pixelSequence != NULL)
+    {
+      index_.reset(new FragmentIndex(pixelSequence, countFrames_));
+      return;
+    }
+
+    // Extract information about the image structure
+    DicomMap tags;
+    FromDcmtkBridge::ExtractDicomSummary(tags, dataset);
+
+    DicomImageInformation information(tags);
+
+    // Access to the raw pixel data
+    if (DicomImageDecoder::IsPsmctRle1(dataset))
+    {
+      index_.reset(new PsmctRle1Index(dataset, countFrames_, information.GetFrameSize()));
+    }
+    else
+    {
+      index_.reset(new UncompressedIndex(dataset, countFrames_, information.GetFrameSize()));
+    }
+  }
+
+
+  void DicomFrameIndex::GetRawFrame(std::string& frame,
+                                    unsigned int index) const
+  {
+    if (index >= countFrames_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else if (index_.get() != NULL)
+    {
+      return index_->GetRawFrame(frame, index);
+    }
+    else
+    {
+      frame.clear();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/Internals/DicomFrameIndex.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Enumerations.h"
+
+#include <dcmtk/dcmdata/dcdatset.h>
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <vector>
+#include <stdint.h>
+#include <boost/noncopyable.hpp>
+#include <memory>
+
+namespace Orthanc
+{
+  class DicomFrameIndex
+  {
+  private:
+    class IIndex : public boost::noncopyable
+    {
+    public:
+      virtual ~IIndex()
+      {
+      }
+
+      virtual void GetRawFrame(std::string& frame,
+                               unsigned int index) const = 0;
+    };
+
+    class FragmentIndex;
+    class UncompressedIndex;
+    class PsmctRle1Index;
+
+    std::auto_ptr<IIndex>  index_;
+    unsigned int           countFrames_;
+
+  public:
+    DicomFrameIndex(DcmFileFormat& dicom);
+
+    unsigned int GetFramesCount() const
+    {
+      return countFrames_;
+    }
+
+    void GetRawFrame(std::string& frame,
+                     unsigned int index) const;
+
+    static bool IsVideo(DcmFileFormat& dicom);
+
+    static unsigned int GetFramesCount(DcmFileFormat& dicom);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/Internals/DicomImageDecoder.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,1001 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "DicomImageDecoder.h"
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project
+  (cf. function "DecodePsmctRle1()"):
+
+  Program: GDCM (Grassroots DICOM). A DICOM library
+  Module:  http://gdcm.sourceforge.net/Copyright.html
+
+  Copyright (c) 2006-2011 Mathieu Malaterre
+  Copyright (c) 1993-2005 CREATIS
+  (CREATIS = Centre de Recherche et d'Applications en Traitement de l'Image)
+  All rights reserved.
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice,
+  this list of conditions and the following disclaimer.
+
+  * Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+  * Neither name of Mathieu Malaterre, or CREATIS, nor the names of any
+  contributors (CNRS, INSERM, UCB, Universite Lyon I), may be used to
+  endorse or promote products derived from this software without specific
+  prior written permission.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
+  ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+  =========================================================================*/
+
+
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+#include "../../Images/Image.h"
+#include "../../Images/ImageProcessing.h"
+#include "../../DicomFormat/DicomIntegerPixelAccessor.h"
+#include "../ToDcmtkBridge.h"
+#include "../FromDcmtkBridge.h"
+#include "../ParsedDicomFile.h"
+
+#if ORTHANC_ENABLE_PNG == 1
+#  include "../../Images/PngWriter.h"
+#endif
+
+#if ORTHANC_ENABLE_JPEG == 1
+#  include "../../Images/JpegWriter.h"
+#endif
+#include "../../Images/PamWriter.h"
+
+#include <boost/lexical_cast.hpp>
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcrleccd.h>
+#include <dcmtk/dcmdata/dcrlecp.h>
+#include <dcmtk/dcmdata/dcrlerp.h>
+
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+#  include <dcmtk/dcmjpeg/djrplol.h>
+#  include <dcmtk/dcmjpls/djcodecd.h>
+#  include <dcmtk/dcmjpls/djcparam.h>
+#  include <dcmtk/dcmjpls/djrparam.h>
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+#  include <dcmtk/dcmjpeg/djcodecd.h>
+#  include <dcmtk/dcmjpeg/djcparam.h>
+#  include <dcmtk/dcmjpeg/djdecbas.h>
+#  include <dcmtk/dcmjpeg/djdecext.h>
+#  include <dcmtk/dcmjpeg/djdeclol.h>
+#  include <dcmtk/dcmjpeg/djdecpro.h>
+#  include <dcmtk/dcmjpeg/djdecsps.h>
+#  include <dcmtk/dcmjpeg/djdecsv1.h>
+#  include <dcmtk/dcmjpeg/djrploss.h>
+#endif
+
+#if DCMTK_VERSION_NUMBER <= 360
+#  define EXS_JPEGProcess1      EXS_JPEGProcess1TransferSyntax
+#  define EXS_JPEGProcess2_4    EXS_JPEGProcess2_4TransferSyntax
+#  define EXS_JPEGProcess6_8    EXS_JPEGProcess6_8TransferSyntax
+#  define EXS_JPEGProcess10_12  EXS_JPEGProcess10_12TransferSyntax
+#  define EXS_JPEGProcess14     EXS_JPEGProcess14TransferSyntax
+#  define EXS_JPEGProcess14SV1  EXS_JPEGProcess14SV1TransferSyntax
+#endif
+
+namespace Orthanc
+{
+  static const DicomTag DICOM_TAG_CONTENT(0x07a1, 0x100a);
+  static const DicomTag DICOM_TAG_COMPRESSION_TYPE(0x07a1, 0x1011);
+
+
+  bool DicomImageDecoder::IsPsmctRle1(DcmDataset& dataset)
+  {
+    DcmElement* e;
+    char* c;
+
+    // Check whether the DICOM instance contains an image encoded with
+    // the PMSCT_RLE1 scheme.
+    if (!dataset.findAndGetElement(ToDcmtkBridge::Convert(DICOM_TAG_COMPRESSION_TYPE), e).good() ||
+        e == NULL ||
+        !e->isaString() ||
+        !e->getString(c).good() ||
+        c == NULL ||
+        strcmp("PMSCT_RLE1", c))
+    {
+      return false;
+    }
+    else
+    {
+      return true;
+    }
+  }
+
+
+  bool DicomImageDecoder::DecodePsmctRle1(std::string& output,
+                                          DcmDataset& dataset)
+  {
+    // Check whether the DICOM instance contains an image encoded with
+    // the PMSCT_RLE1 scheme.
+    if (!IsPsmctRle1(dataset))
+    {
+      return false;
+    }
+
+    // OK, this is a custom RLE encoding from Philips. Get the pixel
+    // data from the appropriate private DICOM tag.
+    Uint8* pixData = NULL;
+    DcmElement* e;
+    if (!dataset.findAndGetElement(ToDcmtkBridge::Convert(DICOM_TAG_CONTENT), e).good() ||
+        e == NULL ||
+        e->getUint8Array(pixData) != EC_Normal)
+    {
+      return false;
+    }    
+
+    // The "unsigned" below IS VERY IMPORTANT
+    const uint8_t* inbuffer = reinterpret_cast<const uint8_t*>(pixData);
+    const size_t length = e->getLength();
+
+    /**
+     * The code below is an adaptation of a sample code for GDCM by
+     * Mathieu Malaterre (under a BSD license).
+     * http://gdcm.sourceforge.net/html/rle2img_8cxx-example.html
+     **/
+
+    // RLE pass
+    std::vector<uint8_t> temp;
+    temp.reserve(length);
+    for (size_t i = 0; i < length; i++)
+    {
+      if (inbuffer[i] == 0xa5)
+      {
+        temp.push_back(inbuffer[i+2]);
+        for (uint8_t repeat = inbuffer[i + 1]; repeat != 0; repeat--)
+        {
+          temp.push_back(inbuffer[i+2]);
+        }
+        i += 2;
+      }
+      else
+      {
+        temp.push_back(inbuffer[i]);
+      }
+    }
+
+    // Delta encoding pass
+    uint16_t delta = 0;
+    output.clear();
+    output.reserve(temp.size());
+    for (size_t i = 0; i < temp.size(); i++)
+    {
+      uint16_t value;
+
+      if (temp[i] == 0x5a)
+      {
+        uint16_t v1 = temp[i + 1];
+        uint16_t v2 = temp[i + 2];
+        value = (v2 << 8) + v1;
+        i += 2;
+      }
+      else
+      {
+        value = delta + (int8_t) temp[i];
+      }
+
+      output.push_back(value & 0xff);
+      output.push_back(value >> 8);
+      delta = value;
+    }
+
+    if (output.size() % 2)
+    {
+      output.resize(output.size() - 1);
+    }
+
+    return true;
+  }
+
+
+  class DicomImageDecoder::ImageSource
+  {
+  private:
+    std::string psmct_;
+    std::auto_ptr<DicomIntegerPixelAccessor> slowAccessor_;
+
+  public:
+    void Setup(DcmDataset& dataset,
+               unsigned int frame)
+    {
+      psmct_.clear();
+      slowAccessor_.reset(NULL);
+
+      // See also: http://support.dcmtk.org/wiki/dcmtk/howto/accessing-compressed-data
+
+      DicomMap m;
+      FromDcmtkBridge::ExtractDicomSummary(m, dataset);
+
+      /**
+       * Create an accessor to the raw values of the DICOM image.
+       **/
+
+      DcmElement* e;
+      if (dataset.findAndGetElement(ToDcmtkBridge::Convert(DICOM_TAG_PIXEL_DATA), e).good() &&
+          e != NULL)
+      {
+        Uint8* pixData = NULL;
+        if (e->getUint8Array(pixData) == EC_Normal)
+        {    
+          slowAccessor_.reset(new DicomIntegerPixelAccessor(m, pixData, e->getLength()));
+        }
+      }
+      else if (DecodePsmctRle1(psmct_, dataset))
+      {
+        LOG(INFO) << "The PMSCT_RLE1 decoding has succeeded";
+        Uint8* pixData = NULL;
+        if (psmct_.size() > 0)
+        {
+          pixData = reinterpret_cast<Uint8*>(&psmct_[0]);
+        }
+
+        slowAccessor_.reset(new DicomIntegerPixelAccessor(m, pixData, psmct_.size()));
+      }
+    
+      if (slowAccessor_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      slowAccessor_->SetCurrentFrame(frame);
+    }
+
+    unsigned int GetWidth() const
+    {
+      assert(slowAccessor_.get() != NULL);
+      return slowAccessor_->GetInformation().GetWidth();
+    }
+
+    unsigned int GetHeight() const
+    {
+      assert(slowAccessor_.get() != NULL);
+      return slowAccessor_->GetInformation().GetHeight();
+    }
+
+    unsigned int GetChannelCount() const
+    {
+      assert(slowAccessor_.get() != NULL);
+      return slowAccessor_->GetInformation().GetChannelCount();
+    }
+
+    const DicomIntegerPixelAccessor& GetAccessor() const
+    {
+      assert(slowAccessor_.get() != NULL);
+      return *slowAccessor_;
+    }
+
+    unsigned int GetSize() const
+    {
+      assert(slowAccessor_.get() != NULL);
+      return slowAccessor_->GetSize();
+    }
+  };
+
+
+  ImageAccessor* DicomImageDecoder::CreateImage(DcmDataset& dataset,
+                                                bool ignorePhotometricInterpretation)
+  {
+    DicomMap m;
+    FromDcmtkBridge::ExtractDicomSummary(m, dataset);
+
+    DicomImageInformation info(m);
+    PixelFormat format;
+    
+    if (!info.ExtractPixelFormat(format, ignorePhotometricInterpretation))
+    {
+      LOG(WARNING) << "Unsupported DICOM image: " << info.GetBitsStored() 
+                   << "bpp, " << info.GetChannelCount() << " channels, " 
+                   << (info.IsSigned() ? "signed" : "unsigned")
+                   << (info.IsPlanar() ? ", planar, " : ", non-planar, ")
+                   << EnumerationToString(info.GetPhotometricInterpretation())
+                   << " photometric interpretation";
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    return new Image(format, info.GetWidth(), info.GetHeight(), false);
+  }
+
+
+  template <typename PixelType>
+  static void CopyPixels(ImageAccessor& target,
+                         const DicomIntegerPixelAccessor& source)
+  {
+    const PixelType minValue = std::numeric_limits<PixelType>::min();
+    const PixelType maxValue = std::numeric_limits<PixelType>::max();
+
+    for (unsigned int y = 0; y < source.GetInformation().GetHeight(); y++)
+    {
+      PixelType* pixel = reinterpret_cast<PixelType*>(target.GetRow(y));
+      for (unsigned int x = 0; x < source.GetInformation().GetWidth(); x++)
+      {
+        for (unsigned int c = 0; c < source.GetInformation().GetChannelCount(); c++, pixel++)
+        {
+          int32_t v = source.GetValue(x, y, c);
+          if (v < static_cast<int32_t>(minValue))
+          {
+            *pixel = minValue;
+          }
+          else if (v > static_cast<int32_t>(maxValue))
+          {
+            *pixel = maxValue;
+          }
+          else
+          {
+            *pixel = static_cast<PixelType>(v);
+          }
+        }
+      }
+    }
+  }
+
+
+  static ImageAccessor* DecodeLookupTable(std::auto_ptr<ImageAccessor>& target,
+                                          const DicomImageInformation& info,
+                                          DcmDataset& dataset,
+                                          const uint8_t* pixelData,
+                                          unsigned long pixelLength)
+  {
+    LOG(INFO) << "Decoding a lookup table";
+
+    OFString r, g, b;
+    PixelFormat format;
+    const uint16_t* lutRed = NULL;
+    const uint16_t* lutGreen = NULL;
+    const uint16_t* lutBlue = NULL;
+    unsigned long rc = 0;
+    unsigned long gc = 0;
+    unsigned long bc = 0;
+
+    if (pixelData == NULL &&
+        !dataset.findAndGetUint8Array(DCM_PixelData, pixelData, &pixelLength).good())
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    if (info.IsPlanar() ||
+        info.GetNumberOfFrames() != 1 ||
+        !info.ExtractPixelFormat(format, false) ||
+        !dataset.findAndGetOFStringArray(DCM_BluePaletteColorLookupTableDescriptor, b).good() ||
+        !dataset.findAndGetOFStringArray(DCM_GreenPaletteColorLookupTableDescriptor, g).good() ||
+        !dataset.findAndGetOFStringArray(DCM_RedPaletteColorLookupTableDescriptor, r).good() ||
+        !dataset.findAndGetUint16Array(DCM_BluePaletteColorLookupTableData, lutBlue, &bc).good() ||
+        !dataset.findAndGetUint16Array(DCM_GreenPaletteColorLookupTableData, lutGreen, &gc).good() ||
+        !dataset.findAndGetUint16Array(DCM_RedPaletteColorLookupTableData, lutRed, &rc).good() ||
+        r != g ||
+        r != b ||
+        g != b ||
+        lutRed == NULL ||
+        lutGreen == NULL ||
+        lutBlue == NULL ||
+        pixelData == NULL)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    switch (format)
+    {
+      case PixelFormat_RGB24:
+      {
+        if (r != "256\\0\\16" ||
+            rc != 256 ||
+            gc != 256 ||
+            bc != 256 ||
+            pixelLength != target->GetWidth() * target->GetHeight())
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+
+        const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData);
+        
+        for (unsigned int y = 0; y < target->GetHeight(); y++)
+        {
+          uint8_t* p = reinterpret_cast<uint8_t*>(target->GetRow(y));
+
+          for (unsigned int x = 0; x < target->GetWidth(); x++)
+          {
+            p[0] = lutRed[*source] >> 8;
+            p[1] = lutGreen[*source] >> 8;
+            p[2] = lutBlue[*source] >> 8;
+            source++;
+            p += 3;
+          }
+        }
+
+        return target.release();
+      }
+
+      case PixelFormat_RGB48:
+      {
+        if (r != "0\\0\\16" ||
+            rc != 65536 ||
+            gc != 65536 ||
+            bc != 65536 ||
+            pixelLength != 2 * target->GetWidth() * target->GetHeight())
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+
+        const uint16_t* source = reinterpret_cast<const uint16_t*>(pixelData);
+        
+        for (unsigned int y = 0; y < target->GetHeight(); y++)
+        {
+          uint16_t* p = reinterpret_cast<uint16_t*>(target->GetRow(y));
+
+          for (unsigned int x = 0; x < target->GetWidth(); x++)
+          {
+            p[0] = lutRed[*source];
+            p[1] = lutGreen[*source];
+            p[2] = lutBlue[*source];
+            source++;
+            p += 3;
+          }
+        }
+
+        return target.release();
+      }
+
+      default:
+        break;
+    }
+
+    throw OrthancException(ErrorCode_InternalError);
+  }                                          
+
+
+  ImageAccessor* DicomImageDecoder::DecodeUncompressedImage(DcmDataset& dataset,
+                                                            unsigned int frame)
+  {
+    /**
+     * Create the target image.
+     **/
+
+    std::auto_ptr<ImageAccessor> target(CreateImage(dataset, false));
+
+    ImageSource source;
+    source.Setup(dataset, frame);
+
+    if (source.GetWidth() != target->GetWidth() ||
+        source.GetHeight() != target->GetHeight())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    
+    /**
+     * Deal with lookup tables
+     **/
+
+    const DicomImageInformation& info = source.GetAccessor().GetInformation();
+
+    if (info.GetPhotometricInterpretation() == PhotometricInterpretation_Palette)
+    {
+      return DecodeLookupTable(target, info, dataset, NULL, 0);
+    }       
+
+
+    /**
+     * If the format of the DICOM buffer is natively supported, use a
+     * direct access to copy its values.
+     **/
+
+    bool fastVersionSuccess = false;
+    PixelFormat sourceFormat;
+    if (!info.IsPlanar() &&
+        info.ExtractPixelFormat(sourceFormat, false))
+    {
+      try
+      {
+        size_t frameSize = info.GetHeight() * info.GetWidth() * GetBytesPerPixel(sourceFormat);
+        if ((frame + 1) * frameSize <= source.GetSize())
+        {
+          const uint8_t* buffer = reinterpret_cast<const uint8_t*>(source.GetAccessor().GetPixelData());
+
+          ImageAccessor sourceImage;
+          sourceImage.AssignReadOnly(sourceFormat, 
+                                     info.GetWidth(), 
+                                     info.GetHeight(),
+                                     info.GetWidth() * GetBytesPerPixel(sourceFormat),
+                                     buffer + frame * frameSize);
+
+          ImageProcessing::Convert(*target, sourceImage);
+          ImageProcessing::ShiftRight(*target, info.GetShift());
+          fastVersionSuccess = true;
+        }
+      }
+      catch (OrthancException&)
+      {
+        // Unsupported conversion, use the slow version
+      }
+    }
+
+    /**
+     * Slow version : loop over the DICOM buffer, storing its value
+     * into the target image.
+     **/
+
+    if (!fastVersionSuccess)
+    {
+      switch (target->GetFormat())
+      {
+        case PixelFormat_RGB24:
+        case PixelFormat_RGBA32:
+        case PixelFormat_Grayscale8:
+          CopyPixels<uint8_t>(*target, source.GetAccessor());
+          break;
+        
+        case PixelFormat_Grayscale16:
+          CopyPixels<uint16_t>(*target, source.GetAccessor());
+          break;
+
+        case PixelFormat_SignedGrayscale16:
+          CopyPixels<int16_t>(*target, source.GetAccessor());
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    return target.release();
+  }
+
+
+  ImageAccessor* DicomImageDecoder::ApplyCodec
+  (const DcmCodec& codec,
+   const DcmCodecParameter& parameters,
+   const DcmRepresentationParameter& representationParameter,
+   DcmDataset& dataset,
+   unsigned int frame)
+  {
+    DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dataset);
+    if (pixelSequence == NULL)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    DicomMap m;
+    FromDcmtkBridge::ExtractDicomSummary(m, dataset);
+    DicomImageInformation info(m);
+
+    std::auto_ptr<ImageAccessor> target(CreateImage(dataset, true));
+
+    Uint32 startFragment = 0;  // Default 
+    OFString decompressedColorModel;  // Out
+
+    OFCondition c;
+    
+    if (info.GetPhotometricInterpretation() == PhotometricInterpretation_Palette &&
+        info.GetChannelCount() == 1)
+    {
+      std::string uncompressed;
+      uncompressed.resize(info.GetWidth() * info.GetHeight() * info.GetBytesPerValue());
+
+      if (uncompressed.size() == 0 ||
+          !codec.decodeFrame(&representationParameter, 
+                             pixelSequence, &parameters, 
+                             &dataset, frame, startFragment, &uncompressed[0],
+                             uncompressed.size(), decompressedColorModel).good())
+      {
+        LOG(ERROR) << "Cannot decode a palette image";
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      return DecodeLookupTable(target, info, dataset,
+                               reinterpret_cast<const uint8_t*>(uncompressed.c_str()),
+                               uncompressed.size());
+    }
+    else
+    {
+      if (!codec.decodeFrame(&representationParameter, 
+                             pixelSequence, &parameters, 
+                             &dataset, frame, startFragment, target->GetBuffer(), 
+                             target->GetSize(), decompressedColorModel).good())
+      {
+        LOG(ERROR) << "Cannot decode a non-palette image";
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      return target.release();
+    }
+  }
+
+
+  ImageAccessor* DicomImageDecoder::Decode(ParsedDicomFile& dicom,
+                                           unsigned int frame)
+  {
+    DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();
+    E_TransferSyntax syntax = dataset.getOriginalXfer();
+
+    /**
+     * Deal with uncompressed, raw images.
+     * http://support.dcmtk.org/docs/dcxfer_8h-source.html
+     **/
+    if (syntax == EXS_Unknown ||
+        syntax == EXS_LittleEndianImplicit ||
+        syntax == EXS_BigEndianImplicit ||
+        syntax == EXS_LittleEndianExplicit ||
+        syntax == EXS_BigEndianExplicit)
+    {
+      return DecodeUncompressedImage(dataset, frame);
+    }
+
+
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+    /**
+     * Deal with JPEG-LS images.
+     **/
+
+    if (syntax == EXS_JPEGLSLossless ||
+        syntax == EXS_JPEGLSLossy)
+    {
+      // The (2, OFTrue) are the default parameters as found in DCMTK 3.6.2
+      // http://support.dcmtk.org/docs/classDJLSRepresentationParameter.html
+      DJLSRepresentationParameter representationParameter(2, OFTrue);
+
+      DJLSCodecParameter parameters;
+      std::auto_ptr<DJLSDecoderBase> decoder;
+
+      switch (syntax)
+      {
+        case EXS_JPEGLSLossless:
+          LOG(INFO) << "Decoding a JPEG-LS lossless DICOM image";
+          decoder.reset(new DJLSLosslessDecoder);
+          break;
+          
+        case EXS_JPEGLSLossy:
+          LOG(INFO) << "Decoding a JPEG-LS near-lossless DICOM image";
+          decoder.reset(new DJLSNearLosslessDecoder);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    
+      return ApplyCodec(*decoder, parameters, representationParameter, dataset, frame);
+    }
+#endif
+
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    /**
+     * Deal with JPEG images.
+     **/
+
+    if (syntax == EXS_JPEGProcess1     ||  // DJDecoderBaseline
+        syntax == EXS_JPEGProcess2_4   ||  // DJDecoderExtended
+        syntax == EXS_JPEGProcess6_8   ||  // DJDecoderSpectralSelection (retired)
+        syntax == EXS_JPEGProcess10_12 ||  // DJDecoderProgressive (retired)
+        syntax == EXS_JPEGProcess14    ||  // DJDecoderLossless
+        syntax == EXS_JPEGProcess14SV1)    // DJDecoderP14SV1
+    {
+      // http://support.dcmtk.org/docs-snapshot/djutils_8h.html#a2a9695e5b6b0f5c45a64c7f072c1eb9d
+      DJCodecParameter parameters(
+        ECC_lossyYCbCr,  // Mode for color conversion for compression, Unused for decompression
+        EDC_photometricInterpretation,  // Perform color space conversion from YCbCr to RGB if DICOM photometric interpretation indicates YCbCr
+        EUC_default,     // Mode for UID creation, unused for decompression
+        EPC_default);    // Automatically determine whether color-by-plane is required from the SOP Class UID and decompressed photometric interpretation
+      DJ_RPLossy representationParameter;
+      std::auto_ptr<DJCodecDecoder> decoder;
+
+      switch (syntax)
+      {
+        case EXS_JPEGProcess1:
+          LOG(INFO) << "Decoding a JPEG baseline (process 1) DICOM image";
+          decoder.reset(new DJDecoderBaseline);
+          break;
+          
+        case EXS_JPEGProcess2_4 :
+          LOG(INFO) << "Decoding a JPEG baseline (processes 2 and 4) DICOM image";
+          decoder.reset(new DJDecoderExtended);
+          break;
+          
+        case EXS_JPEGProcess6_8:   // Retired
+          LOG(INFO) << "Decoding a JPEG spectral section, nonhierarchical (processes 6 and 8) DICOM image";
+          decoder.reset(new DJDecoderSpectralSelection);
+          break;
+          
+        case EXS_JPEGProcess10_12:   // Retired
+          LOG(INFO) << "Decoding a JPEG full progression, nonhierarchical (processes 10 and 12) DICOM image";
+          decoder.reset(new DJDecoderProgressive);
+          break;
+          
+        case EXS_JPEGProcess14:
+          LOG(INFO) << "Decoding a JPEG lossless, nonhierarchical (process 14) DICOM image";
+          decoder.reset(new DJDecoderLossless);
+          break;
+          
+        case EXS_JPEGProcess14SV1:
+          LOG(INFO) << "Decoding a JPEG lossless, nonhierarchical, first-order prediction (process 14 selection value 1) DICOM image";
+          decoder.reset(new DJDecoderP14SV1);
+          break;
+          
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    
+      return ApplyCodec(*decoder, parameters, representationParameter, dataset, frame);      
+    }
+#endif
+
+
+    if (syntax == EXS_RLELossless)
+    {
+      LOG(INFO) << "Decoding a RLE lossless DICOM image";
+      DcmRLECodecParameter parameters;
+      DcmRLECodecDecoder decoder;
+      DcmRLERepresentationParameter representationParameter;
+      return ApplyCodec(decoder, parameters, representationParameter, dataset, frame);
+    }
+
+
+    /**
+     * This DICOM image format is not natively supported by
+     * Orthanc. As a last resort, try and decode it through DCMTK by
+     * converting its transfer syntax to Little Endian. This will
+     * result in higher memory consumption. This is actually the
+     * second example of the following page:
+     * http://support.dcmtk.org/docs/mod_dcmjpeg.html#Examples
+     **/
+    
+    {
+      LOG(INFO) << "Decoding a compressed image by converting its transfer syntax to Little Endian";
+
+      std::auto_ptr<DcmDataset> converted(dynamic_cast<DcmDataset*>(dataset.clone()));
+      converted->chooseRepresentation(EXS_LittleEndianExplicit, NULL);
+
+      if (converted->canWriteXfer(EXS_LittleEndianExplicit))
+      {
+        return DecodeUncompressedImage(*converted, frame);
+      }
+    }
+
+    LOG(ERROR) << "Cannot decode a DICOM image with the built-in decoder";
+    throw OrthancException(ErrorCode_BadFileFormat);
+  }
+
+
+  static bool IsColorImage(PixelFormat format)
+  {
+    return (format == PixelFormat_RGB24 ||
+            format == PixelFormat_RGBA32);
+  }
+
+
+  bool DicomImageDecoder::TruncateDecodedImage(std::auto_ptr<ImageAccessor>& image,
+                                               PixelFormat format,
+                                               bool allowColorConversion)
+  {
+    // If specified, prevent the conversion between color and
+    // grayscale images
+    bool isSourceColor = IsColorImage(image->GetFormat());
+    bool isTargetColor = IsColorImage(format);
+
+    if (!allowColorConversion)
+    {
+      if (isSourceColor ^ isTargetColor)
+      {
+        return false;
+      }
+    }
+
+    if (image->GetFormat() != format)
+    {
+      // A conversion is required
+      std::auto_ptr<ImageAccessor> target
+        (new Image(format, image->GetWidth(), image->GetHeight(), false));
+      ImageProcessing::Convert(*target, *image);
+      image = target;
+    }
+
+    return true;
+  }
+
+
+  bool DicomImageDecoder::PreviewDecodedImage(std::auto_ptr<ImageAccessor>& image)
+  {
+    switch (image->GetFormat())
+    {
+      case PixelFormat_RGB24:
+      {
+        // Directly return color images without modification (RGB)
+        return true;
+      }
+
+      case PixelFormat_RGB48:
+      {
+        std::auto_ptr<ImageAccessor> target
+          (new Image(PixelFormat_RGB24, image->GetWidth(), image->GetHeight(), false));
+        ImageProcessing::Convert(*target, *image);
+        image = target;
+        return true;
+      }
+
+      case PixelFormat_Grayscale8:
+      case PixelFormat_Grayscale16:
+      case PixelFormat_SignedGrayscale16:
+      {
+        // Grayscale image: Stretch its dynamics to the [0,255] range
+        int64_t a, b;
+        ImageProcessing::GetMinMaxIntegerValue(a, b, *image);
+
+        if (a == b)
+        {
+          ImageProcessing::Set(*image, 0);
+        }
+        else
+        {
+          ImageProcessing::ShiftScale(*image, static_cast<float>(-a),
+                                      255.0f / static_cast<float>(b - a),
+                                      true /* TODO - Consider using "false" to speed up */);
+        }
+
+        // If the source image is not grayscale 8bpp, convert it
+        if (image->GetFormat() != PixelFormat_Grayscale8)
+        {
+          std::auto_ptr<ImageAccessor> target
+            (new Image(PixelFormat_Grayscale8, image->GetWidth(), image->GetHeight(), false));
+          ImageProcessing::Convert(*target, *image);
+          image = target;
+        }
+
+        return true;
+      }
+      
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void DicomImageDecoder::ApplyExtractionMode(std::auto_ptr<ImageAccessor>& image,
+                                              ImageExtractionMode mode,
+                                              bool invert)
+  {
+    if (image.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    bool ok = false;
+
+    switch (mode)
+    {
+      case ImageExtractionMode_UInt8:
+        ok = TruncateDecodedImage(image, PixelFormat_Grayscale8, false);
+        break;
+
+      case ImageExtractionMode_UInt16:
+        ok = TruncateDecodedImage(image, PixelFormat_Grayscale16, false);
+        break;
+
+      case ImageExtractionMode_Int16:
+        ok = TruncateDecodedImage(image, PixelFormat_SignedGrayscale16, false);
+        break;
+
+      case ImageExtractionMode_Preview:
+        ok = PreviewDecodedImage(image);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (ok)
+    {
+      assert(image.get() != NULL);
+
+      if (invert)
+      {
+        Orthanc::ImageProcessing::Invert(*image);
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void DicomImageDecoder::ExtractPamImage(std::string& result,
+                                          std::auto_ptr<ImageAccessor>& image,
+                                          ImageExtractionMode mode,
+                                          bool invert)
+  {
+    ApplyExtractionMode(image, mode, invert);
+
+    PamWriter writer;
+    writer.WriteToMemory(result, *image);
+  }
+
+#if ORTHANC_ENABLE_PNG == 1
+  void DicomImageDecoder::ExtractPngImage(std::string& result,
+                                          std::auto_ptr<ImageAccessor>& image,
+                                          ImageExtractionMode mode,
+                                          bool invert)
+  {
+    ApplyExtractionMode(image, mode, invert);
+
+    PngWriter writer;
+    writer.WriteToMemory(result, *image);
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_JPEG == 1
+  void DicomImageDecoder::ExtractJpegImage(std::string& result,
+                                           std::auto_ptr<ImageAccessor>& image,
+                                           ImageExtractionMode mode,
+                                           bool invert,
+                                           uint8_t quality)
+  {
+    if (mode != ImageExtractionMode_UInt8 &&
+        mode != ImageExtractionMode_Preview)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    ApplyExtractionMode(image, mode, invert);
+
+    JpegWriter writer;
+    writer.SetQuality(quality);
+    writer.WriteToMemory(result, *image);
+  }
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/Internals/DicomImageDecoder.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../ParsedDicomFile.h"
+
+#include <memory>
+
+#if !defined(ORTHANC_ENABLE_JPEG)
+#  error The macro ORTHANC_ENABLE_JPEG must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_PNG)
+#  error The macro ORTHANC_ENABLE_PNG must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_DCMTK_JPEG)
+#  error The macro ORTHANC_ENABLE_DCMTK_JPEG must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS)
+#  error The macro ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS must be defined
+#endif
+
+
+class DcmDataset;
+class DcmCodec;
+class DcmCodecParameter;
+class DcmRepresentationParameter;
+
+namespace Orthanc
+{
+  class DicomImageDecoder : public boost::noncopyable
+  {
+  private:
+    class ImageSource;
+
+    DicomImageDecoder()   // This is a fully abstract class, no constructor
+    {
+    }
+
+    static ImageAccessor* CreateImage(DcmDataset& dataset,
+                                      bool ignorePhotometricInterpretation);
+
+    static ImageAccessor* DecodeUncompressedImage(DcmDataset& dataset,
+                                                  unsigned int frame);
+
+    static ImageAccessor* ApplyCodec(const DcmCodec& codec,
+                                     const DcmCodecParameter& parameters,
+                                     const DcmRepresentationParameter& representationParameter,
+                                     DcmDataset& dataset,
+                                     unsigned int frame);
+
+    static bool TruncateDecodedImage(std::auto_ptr<ImageAccessor>& image,
+                                     PixelFormat format,
+                                     bool allowColorConversion);
+
+    static bool PreviewDecodedImage(std::auto_ptr<ImageAccessor>& image);
+
+    static void ApplyExtractionMode(std::auto_ptr<ImageAccessor>& image,
+                                    ImageExtractionMode mode,
+                                    bool invert);
+
+  public:
+    static bool IsPsmctRle1(DcmDataset& dataset);
+
+    static bool DecodePsmctRle1(std::string& output,
+                                DcmDataset& dataset);
+
+    static ImageAccessor *Decode(ParsedDicomFile& dicom,
+                                 unsigned int frame);
+
+    static void ExtractPamImage(std::string& result,
+                                std::auto_ptr<ImageAccessor>& image,
+                                ImageExtractionMode mode,
+                                bool invert);
+
+#if ORTHANC_ENABLE_PNG == 1
+    static void ExtractPngImage(std::string& result,
+                                std::auto_ptr<ImageAccessor>& image,
+                                ImageExtractionMode mode,
+                                bool invert);
+#endif
+
+#if ORTHANC_ENABLE_JPEG == 1
+    static void ExtractJpegImage(std::string& result,
+                                 std::auto_ptr<ImageAccessor>& image,
+                                 ImageExtractionMode mode,
+                                 bool invert,
+                                 uint8_t quality);
+#endif
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/ParsedDicomFile.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,1549 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: GDCM (Grassroots DICOM). A DICOM library
+  Module:  http://gdcm.sourceforge.net/Copyright.html
+
+  Copyright (c) 2006-2011 Mathieu Malaterre
+  Copyright (c) 1993-2005 CREATIS
+  (CREATIS = Centre de Recherche et d'Applications en Traitement de l'Image)
+  All rights reserved.
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice,
+  this list of conditions and the following disclaimer.
+
+  * Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+  * Neither name of Mathieu Malaterre, or CREATIS, nor the names of any
+  contributors (CNRS, INSERM, UCB, Universite Lyon I), may be used to
+  endorse or promote products derived from this software without specific
+  prior written permission.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
+  ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+  =========================================================================*/
+
+
+#include "../PrecompiledHeaders.h"
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+#include "ParsedDicomFile.h"
+
+#include "FromDcmtkBridge.h"
+#include "ToDcmtkBridge.h"
+#include "Internals/DicomFrameIndex.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#if ORTHANC_SANDBOXED == 0
+#  include "../SystemToolbox.h"
+#endif
+
+#if ORTHANC_ENABLE_JPEG == 1
+#  include "../Images/JpegReader.h"
+#endif
+
+#if ORTHANC_ENABLE_PNG == 1
+#  include "../Images/PngReader.h"
+#endif
+
+#include <list>
+#include <limits>
+
+#include <boost/lexical_cast.hpp>
+
+#include <dcmtk/dcmdata/dcchrstr.h>
+#include <dcmtk/dcmdata/dcdicent.h>
+#include <dcmtk/dcmdata/dcdict.h>
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcuid.h>
+#include <dcmtk/dcmdata/dcmetinf.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+
+#include <dcmtk/dcmdata/dcvrae.h>
+#include <dcmtk/dcmdata/dcvras.h>
+#include <dcmtk/dcmdata/dcvrcs.h>
+#include <dcmtk/dcmdata/dcvrda.h>
+#include <dcmtk/dcmdata/dcvrds.h>
+#include <dcmtk/dcmdata/dcvrdt.h>
+#include <dcmtk/dcmdata/dcvrfd.h>
+#include <dcmtk/dcmdata/dcvrfl.h>
+#include <dcmtk/dcmdata/dcvris.h>
+#include <dcmtk/dcmdata/dcvrlo.h>
+#include <dcmtk/dcmdata/dcvrlt.h>
+#include <dcmtk/dcmdata/dcvrpn.h>
+#include <dcmtk/dcmdata/dcvrsh.h>
+#include <dcmtk/dcmdata/dcvrsl.h>
+#include <dcmtk/dcmdata/dcvrss.h>
+#include <dcmtk/dcmdata/dcvrst.h>
+#include <dcmtk/dcmdata/dcvrtm.h>
+#include <dcmtk/dcmdata/dcvrui.h>
+#include <dcmtk/dcmdata/dcvrul.h>
+#include <dcmtk/dcmdata/dcvrus.h>
+#include <dcmtk/dcmdata/dcvrut.h>
+#include <dcmtk/dcmdata/dcpixel.h>
+#include <dcmtk/dcmdata/dcpixseq.h>
+#include <dcmtk/dcmdata/dcpxitem.h>
+
+
+#include <boost/math/special_functions/round.hpp>
+#include <dcmtk/dcmdata/dcostrmb.h>
+#include <boost/algorithm/string/predicate.hpp>
+
+
+#if DCMTK_VERSION_NUMBER <= 360
+#  define EXS_JPEGProcess1      EXS_JPEGProcess1TransferSyntax
+#endif
+
+
+
+namespace Orthanc
+{
+  struct ParsedDicomFile::PImpl
+  {
+    std::auto_ptr<DcmFileFormat> file_;
+    std::auto_ptr<DicomFrameIndex>  frameIndex_;
+  };
+
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+  static const char* CONTENT_TYPE_OCTET_STREAM = "application/octet-stream";
+
+  static void ParseTagAndGroup(DcmTagKey& key,
+                               const std::string& tag)
+  {
+    DicomTag t = FromDcmtkBridge::ParseTag(tag);
+    key = DcmTagKey(t.GetGroup(), t.GetElement());
+  }
+
+  
+  static unsigned int GetPixelDataBlockCount(DcmPixelData& pixelData,
+                                             E_TransferSyntax transferSyntax)
+  {
+    DcmPixelSequence* pixelSequence = NULL;
+    if (pixelData.getEncapsulatedRepresentation
+        (transferSyntax, NULL, pixelSequence).good() && pixelSequence)
+    {
+      return pixelSequence->card();
+    }
+    else
+    {
+      return 1;
+    }
+  }
+
+  
+  static void SendPathValueForDictionary(RestApiOutput& output,
+                                         DcmItem& dicom)
+  {
+    Json::Value v = Json::arrayValue;
+
+    for (unsigned long i = 0; i < dicom.card(); i++)
+    {
+      DcmElement* element = dicom.getElement(i);
+      if (element)
+      {
+        char buf[16];
+        sprintf(buf, "%04x-%04x", element->getTag().getGTag(), element->getTag().getETag());
+        v.append(buf);
+      }
+    }
+
+    output.AnswerJson(v);
+  }
+
+
+  static void SendSequence(RestApiOutput& output,
+                           DcmSequenceOfItems& sequence)
+  {
+    // This element is a sequence
+    Json::Value v = Json::arrayValue;
+
+    for (unsigned long i = 0; i < sequence.card(); i++)
+    {
+      v.append(boost::lexical_cast<std::string>(i));
+    }
+
+    output.AnswerJson(v);
+  }
+
+
+  namespace
+  {
+    class DicomFieldStream : public IHttpStreamAnswer
+    {
+    private:
+      DcmElement&  element_;
+      uint32_t     length_;
+      uint32_t     offset_;
+      std::string  chunk_;
+      size_t       chunkSize_;
+      
+    public:
+      DicomFieldStream(DcmElement& element,
+                       E_TransferSyntax transferSyntax) :
+        element_(element),
+        length_(element.getLength(transferSyntax)),
+        offset_(0),
+        chunkSize_(0)
+      {
+        static const size_t CHUNK_SIZE = 64 * 1024;  // Use chunks of max 64KB
+        chunk_.resize(CHUNK_SIZE);
+      }
+
+      virtual HttpCompression SetupHttpCompression(bool /*gzipAllowed*/,
+                                                   bool /*deflateAllowed*/)
+      {
+        // No support for compression
+        return HttpCompression_None;
+      }
+
+      virtual bool HasContentFilename(std::string& filename)
+      {
+        return false;
+      }
+
+      virtual std::string GetContentType()
+      {
+        return "";
+      }
+
+      virtual uint64_t  GetContentLength()
+      {
+        return length_;
+      }
+ 
+      virtual bool ReadNextChunk()
+      {
+        assert(offset_ <= length_);
+
+        if (offset_ == length_)
+        {
+          return false;
+        }
+        else
+        {
+          if (length_ - offset_ < chunk_.size())
+          {
+            chunkSize_ = length_ - offset_;
+          }
+          else
+          {
+            chunkSize_ = chunk_.size();
+          }
+
+          OFCondition cond = element_.getPartialValue(&chunk_[0], offset_, chunkSize_);
+
+          offset_ += chunkSize_;
+
+          if (!cond.good())
+          {
+            LOG(ERROR) << "Error while sending a DICOM field: " << cond.text();
+            throw OrthancException(ErrorCode_InternalError);
+          }
+
+          return true;
+        }
+      }
+ 
+      virtual const char *GetChunkContent()
+      {
+        return chunk_.c_str();
+      }
+ 
+      virtual size_t GetChunkSize()
+      {
+        return chunkSize_;
+      }
+    };
+  }
+
+
+  static bool AnswerPixelData(RestApiOutput& output,
+                              DcmItem& dicom,
+                              E_TransferSyntax transferSyntax,
+                              const std::string* blockUri)
+  {
+    DcmTag k(DICOM_TAG_PIXEL_DATA.GetGroup(),
+             DICOM_TAG_PIXEL_DATA.GetElement());
+
+    DcmElement *element = NULL;
+    if (!dicom.findAndGetElement(k, element).good() ||
+        element == NULL)
+    {
+      return false;
+    }
+
+    try
+    {
+      DcmPixelData& pixelData = dynamic_cast<DcmPixelData&>(*element);
+      if (blockUri == NULL)
+      {
+        // The user asks how many blocks are present in this pixel data
+        unsigned int blocks = GetPixelDataBlockCount(pixelData, transferSyntax);
+
+        Json::Value result(Json::arrayValue);
+        for (unsigned int i = 0; i < blocks; i++)
+        {
+          result.append(boost::lexical_cast<std::string>(i));
+        }
+        
+        output.AnswerJson(result);
+        return true;
+      }
+
+
+      unsigned int block = boost::lexical_cast<unsigned int>(*blockUri);
+
+      if (block < GetPixelDataBlockCount(pixelData, transferSyntax))
+      {
+        DcmPixelSequence* pixelSequence = NULL;
+        if (pixelData.getEncapsulatedRepresentation
+            (transferSyntax, NULL, pixelSequence).good() && pixelSequence)
+        {
+          // This is the case for JPEG transfer syntaxes
+          if (block < pixelSequence->card())
+          {
+            DcmPixelItem* pixelItem = NULL;
+            if (pixelSequence->getItem(pixelItem, block).good() && pixelItem)
+            {
+              if (pixelItem->getLength() == 0)
+              {
+                output.AnswerBuffer(NULL, 0, CONTENT_TYPE_OCTET_STREAM);
+                return true;
+              }
+
+              Uint8* buffer = NULL;
+              if (pixelItem->getUint8Array(buffer).good() && buffer)
+              {
+                output.AnswerBuffer(buffer, pixelItem->getLength(), CONTENT_TYPE_OCTET_STREAM);
+                return true;
+              }
+            }
+          }
+        }
+        else
+        {
+          // This is the case for raw, uncompressed image buffers
+          assert(*blockUri == "0");
+          DicomFieldStream stream(*element, transferSyntax);
+          output.AnswerStream(stream);
+        }
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      // The URI entered by the user is not a number
+    }
+    catch (std::bad_cast&)
+    {
+      // This should never happen
+    }
+
+    return false;
+  }
+
+
+  static void SendPathValueForLeaf(RestApiOutput& output,
+                                   const std::string& tag,
+                                   DcmItem& dicom,
+                                   E_TransferSyntax transferSyntax)
+  {
+    DcmTagKey k;
+    ParseTagAndGroup(k, tag);
+
+    DcmSequenceOfItems* sequence = NULL;
+    if (dicom.findAndGetSequence(k, sequence).good() && 
+        sequence != NULL &&
+        sequence->getVR() == EVR_SQ)
+    {
+      SendSequence(output, *sequence);
+      return;
+    }
+
+    DcmElement* element = NULL;
+    if (dicom.findAndGetElement(k, element).good() && 
+        element != NULL &&
+        //element->getVR() != EVR_UNKNOWN &&  // This would forbid private tags
+        element->getVR() != EVR_SQ)
+    {
+      DicomFieldStream stream(*element, transferSyntax);
+      output.AnswerStream(stream);
+    }
+  }
+#endif
+
+  
+  static inline uint16_t GetCharValue(char c)
+  {
+    if (c >= '0' && c <= '9')
+      return c - '0';
+    else if (c >= 'a' && c <= 'f')
+      return c - 'a' + 10;
+    else if (c >= 'A' && c <= 'F')
+      return c - 'A' + 10;
+    else
+      return 0;
+  }
+
+  
+  static inline uint16_t GetTagValue(const char* c)
+  {
+    return ((GetCharValue(c[0]) << 12) + 
+            (GetCharValue(c[1]) << 8) + 
+            (GetCharValue(c[2]) << 4) + 
+            GetCharValue(c[3]));
+  }
+
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+  void ParsedDicomFile::SendPathValue(RestApiOutput& output,
+                                      const UriComponents& uri)
+  {
+    DcmItem* dicom = pimpl_->file_->getDataset();
+    E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getOriginalXfer();
+
+    // Special case: Accessing the pixel data
+    if (uri.size() == 1 || 
+        uri.size() == 2)
+    {
+      DcmTagKey tag;
+      ParseTagAndGroup(tag, uri[0]);
+
+      if (tag.getGroup() == DICOM_TAG_PIXEL_DATA.GetGroup() &&
+          tag.getElement() == DICOM_TAG_PIXEL_DATA.GetElement())
+      {
+        AnswerPixelData(output, *dicom, transferSyntax, uri.size() == 1 ? NULL : &uri[1]);
+        return;
+      }
+    }        
+
+    // Go down in the tag hierarchy according to the URI
+    for (size_t pos = 0; pos < uri.size() / 2; pos++)
+    {
+      size_t index;
+      try
+      {
+        index = boost::lexical_cast<size_t>(uri[2 * pos + 1]);
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        return;
+      }
+
+      DcmTagKey k;
+      DcmItem *child = NULL;
+      ParseTagAndGroup(k, uri[2 * pos]);
+      if (!dicom->findAndGetSequenceItem(k, child, index).good() ||
+          child == NULL)
+      {
+        return;
+      }
+
+      dicom = child;
+    }
+
+    // We have reached the end of the URI
+    if (uri.size() % 2 == 0)
+    {
+      SendPathValueForDictionary(output, *dicom);
+    }
+    else
+    {
+      SendPathValueForLeaf(output, uri.back(), *dicom, transferSyntax);
+    }
+  }
+#endif
+  
+
+  void ParsedDicomFile::Remove(const DicomTag& tag)
+  {
+    InvalidateCache();
+
+    DcmTagKey key(tag.GetGroup(), tag.GetElement());
+    DcmElement* element = pimpl_->file_->getDataset()->remove(key);
+    if (element != NULL)
+    {
+      delete element;
+    }
+  }
+
+
+  void ParsedDicomFile::Clear(const DicomTag& tag,
+                              bool onlyIfExists)
+  {
+    if (tag.GetElement() == 0x0000)
+    {
+      // Prevent manually modifying generic group length tags: This is
+      // handled by DCMTK serialization
+      return;
+    }
+
+    InvalidateCache();
+
+    DcmItem* dicom = pimpl_->file_->getDataset();
+    DcmTagKey key(tag.GetGroup(), tag.GetElement());
+
+    if (onlyIfExists &&
+        !dicom->tagExists(key))
+    {
+      // The tag is non-existing, do not clear it
+    }
+    else
+    {
+      if (!dicom->insertEmptyElement(key, OFTrue /* replace old value */).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  void ParsedDicomFile::RemovePrivateTagsInternal(const std::set<DicomTag>* toKeep)
+  {
+    InvalidateCache();
+
+    DcmDataset& dataset = *pimpl_->file_->getDataset();
+
+    // Loop over the dataset to detect its private tags
+    typedef std::list<DcmElement*> Tags;
+    Tags privateTags;
+
+    for (unsigned long i = 0; i < dataset.card(); i++)
+    {
+      DcmElement* element = dataset.getElement(i);
+      DcmTag tag(element->getTag());
+
+      // Is this a private tag?
+      if (tag.isPrivate())
+      {
+        bool remove = true;
+
+        // Check whether this private tag is to be kept
+        if (toKeep != NULL)
+        {
+          DicomTag tmp = FromDcmtkBridge::Convert(tag);
+          if (toKeep->find(tmp) != toKeep->end())
+          {
+            remove = false;  // Keep it
+          }
+        }
+            
+        if (remove)
+        {
+          privateTags.push_back(element);
+        }
+      }
+    }
+
+    // Loop over the detected private tags to remove them
+    for (Tags::iterator it = privateTags.begin(); 
+         it != privateTags.end(); ++it)
+    {
+      DcmElement* tmp = dataset.remove(*it);
+      if (tmp != NULL)
+      {
+        delete tmp;
+      }
+    }
+  }
+
+
+  static void InsertInternal(DcmDataset& dicom,
+                             DcmElement* element)
+  {
+    OFCondition cond = dicom.insert(element, false, false);
+    if (!cond.good())
+    {
+      // This field already exists
+      delete element;
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  void ParsedDicomFile::Insert(const DicomTag& tag,
+                               const Json::Value& value,
+                               bool decodeDataUriScheme)
+  {
+    if (tag.GetElement() == 0x0000)
+    {
+      // Prevent manually modifying generic group length tags: This is
+      // handled by DCMTK serialization
+      return;
+    }
+
+    if (pimpl_->file_->getDataset()->tagExists(ToDcmtkBridge::Convert(tag)))
+    {
+      throw OrthancException(ErrorCode_AlreadyExistingTag);
+    }
+
+    if (decodeDataUriScheme &&
+        value.type() == Json::stringValue &&
+        (tag == DICOM_TAG_ENCAPSULATED_DOCUMENT ||
+         tag == DICOM_TAG_PIXEL_DATA))
+    {
+      if (EmbedContentInternal(value.asString()))
+      {
+        return;
+      }
+    }
+
+    InvalidateCache();
+    std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, GetEncoding()));
+    InsertInternal(*pimpl_->file_->getDataset(), element.release());
+  }
+
+
+  static bool CanReplaceProceed(DcmDataset& dicom,
+                                const DcmTagKey& tag,
+                                DicomReplaceMode mode)
+  {
+    if (dicom.findAndDeleteElement(tag).good())
+    {
+      // This tag was existing, it has been deleted
+      return true;
+    }
+    else
+    {
+      // This tag was absent, act wrt. the specified "mode"
+      switch (mode)
+      {
+        case DicomReplaceMode_InsertIfAbsent:
+          return true;
+
+        case DicomReplaceMode_ThrowIfAbsent:
+          throw OrthancException(ErrorCode_InexistentItem);
+
+        case DicomReplaceMode_IgnoreIfAbsent:
+          return false;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+  }
+
+
+  void ParsedDicomFile::UpdateStorageUid(const DicomTag& tag,
+                                         const std::string& utf8Value,
+                                         bool decodeDataUriScheme)
+  {
+    if (tag != DICOM_TAG_SOP_CLASS_UID &&
+        tag != DICOM_TAG_SOP_INSTANCE_UID)
+    {
+      return;
+    }
+
+    std::string binary;
+    const std::string* decoded = &utf8Value;
+
+    if (decodeDataUriScheme &&
+        boost::starts_with(utf8Value, "data:application/octet-stream;base64,"))
+    {
+      std::string mime;
+      if (!Toolbox::DecodeDataUriScheme(mime, binary, utf8Value))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      decoded = &binary;
+    }
+    else
+    {
+      Encoding encoding = GetEncoding();
+      if (GetEncoding() != Encoding_Utf8)
+      {
+        binary = Toolbox::ConvertFromUtf8(utf8Value, encoding);
+        decoded = &binary;
+      }
+    }
+
+    /**
+     * dcmodify will automatically correct 'Media Storage SOP Class
+     * UID' and 'Media Storage SOP Instance UID' in the metaheader, if
+     * you make changes to the related tags in the dataset ('SOP Class
+     * UID' and 'SOP Instance UID') via insert or modify mode
+     * options. You can disable this behaviour by using the -nmu
+     * option.
+     **/
+
+    if (tag == DICOM_TAG_SOP_CLASS_UID)
+    {
+      ReplacePlainString(DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID, *decoded);
+    }
+
+    if (tag == DICOM_TAG_SOP_INSTANCE_UID)
+    {
+      ReplacePlainString(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID, *decoded);
+    }    
+  }
+
+
+  void ParsedDicomFile::Replace(const DicomTag& tag,
+                                const std::string& utf8Value,
+                                bool decodeDataUriScheme,
+                                DicomReplaceMode mode)
+  {
+    if (tag.GetElement() == 0x0000)
+    {
+      // Prevent manually modifying generic group length tags: This is
+      // handled by DCMTK serialization
+      return;
+    }
+
+    InvalidateCache();
+
+    DcmDataset& dicom = *pimpl_->file_->getDataset();
+    if (CanReplaceProceed(dicom, ToDcmtkBridge::Convert(tag), mode))
+    {
+      // Either the tag was previously existing (and now removed), or
+      // the replace mode was set to "InsertIfAbsent"
+
+      if (decodeDataUriScheme &&
+          (tag == DICOM_TAG_ENCAPSULATED_DOCUMENT ||
+           tag == DICOM_TAG_PIXEL_DATA))
+      {
+        if (EmbedContentInternal(utf8Value))
+        {
+          return;
+        }
+      }
+
+      std::auto_ptr<DcmElement> element(FromDcmtkBridge::CreateElementForTag(tag));
+      FromDcmtkBridge::FillElementWithString(*element, tag, utf8Value, decodeDataUriScheme, GetEncoding());
+
+      InsertInternal(dicom, element.release());
+      UpdateStorageUid(tag, utf8Value, false);
+    }
+  }
+
+    
+  void ParsedDicomFile::Replace(const DicomTag& tag,
+                                const Json::Value& value,
+                                bool decodeDataUriScheme,
+                                DicomReplaceMode mode)
+  {
+    if (tag.GetElement() == 0x0000)
+    {
+      // Prevent manually modifying generic group length tags: This is
+      // handled by DCMTK serialization
+      return;
+    }
+
+    InvalidateCache();
+
+    DcmDataset& dicom = *pimpl_->file_->getDataset();
+    if (CanReplaceProceed(dicom, ToDcmtkBridge::Convert(tag), mode))
+    {
+      // Either the tag was previously existing (and now removed), or
+      // the replace mode was set to "InsertIfAbsent"
+
+      if (decodeDataUriScheme &&
+          value.type() == Json::stringValue &&
+          (tag == DICOM_TAG_ENCAPSULATED_DOCUMENT ||
+           tag == DICOM_TAG_PIXEL_DATA))
+      {
+        if (EmbedContentInternal(value.asString()))
+        {
+          return;
+        }
+      }
+
+      InsertInternal(dicom, FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, GetEncoding()));
+
+      if (tag == DICOM_TAG_SOP_CLASS_UID ||
+          tag == DICOM_TAG_SOP_INSTANCE_UID)
+      {
+        if (value.type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_BadParameterType);
+        }
+
+        UpdateStorageUid(tag, value.asString(), decodeDataUriScheme);
+      }
+    }
+  }
+
+    
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+  void ParsedDicomFile::Answer(RestApiOutput& output)
+  {
+    std::string serialized;
+    if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *pimpl_->file_->getDataset()))
+    {
+      output.AnswerBuffer(serialized, CONTENT_TYPE_OCTET_STREAM);
+    }
+  }
+#endif
+
+
+  bool ParsedDicomFile::GetTagValue(std::string& value,
+                                    const DicomTag& tag)
+  {
+    DcmTagKey k(tag.GetGroup(), tag.GetElement());
+    DcmDataset& dataset = *pimpl_->file_->getDataset();
+
+    if (tag.IsPrivate() ||
+        FromDcmtkBridge::IsUnknownTag(tag) ||
+        tag == DICOM_TAG_PIXEL_DATA ||
+        tag == DICOM_TAG_ENCAPSULATED_DOCUMENT)
+    {
+      const Uint8* data = NULL;   // This is freed in the destructor of the dataset
+      long unsigned int count = 0;
+
+      if (dataset.findAndGetUint8Array(k, data, &count).good())
+      {
+        if (count > 0)
+        {
+          assert(data != NULL);
+          value.assign(reinterpret_cast<const char*>(data), count);
+        }
+        else
+        {
+          value.clear();
+        }
+
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    else
+    {
+      DcmElement* element = NULL;
+      if (!dataset.findAndGetElement(k, element).good() ||
+          element == NULL)
+      {
+        return false;
+      }
+
+      std::set<DicomTag> tmp;
+      std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement
+                                  (*element, DicomToJsonFlags_Default, 
+                                   0, GetEncoding(), tmp));
+      
+      if (v.get() == NULL ||
+          v->IsNull())
+      {
+        value = "";
+      }
+      else
+      {
+        // TODO v->IsBinary()
+        value = v->GetContent();
+      }
+      
+      return true;
+    }
+  }
+
+
+  DicomInstanceHasher ParsedDicomFile::GetHasher()
+  {
+    std::string patientId, studyUid, seriesUid, instanceUid;
+
+    if (!GetTagValue(patientId, DICOM_TAG_PATIENT_ID) ||
+        !GetTagValue(studyUid, DICOM_TAG_STUDY_INSTANCE_UID) ||
+        !GetTagValue(seriesUid, DICOM_TAG_SERIES_INSTANCE_UID) ||
+        !GetTagValue(instanceUid, DICOM_TAG_SOP_INSTANCE_UID))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    return DicomInstanceHasher(patientId, studyUid, seriesUid, instanceUid);
+  }
+
+
+  void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer)
+  {
+    FromDcmtkBridge::SaveToMemoryBuffer(buffer, *pimpl_->file_->getDataset());
+  }
+
+
+#if ORTHANC_SANDBOXED == 0
+  void ParsedDicomFile::SaveToFile(const std::string& path)
+  {
+    // TODO Avoid using a temporary memory buffer, write directly on disk
+    std::string content;
+    SaveToMemoryBuffer(content);
+    SystemToolbox::WriteFile(content, path);
+  }
+#endif
+
+
+  ParsedDicomFile::ParsedDicomFile(bool createIdentifiers) : pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(new DcmFileFormat);
+
+    if (createIdentifiers)
+    {
+      ReplacePlainString(DICOM_TAG_PATIENT_ID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient));
+      ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study));
+      ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series));
+      ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance));
+    }
+  }
+
+
+  void ParsedDicomFile::CreateFromDicomMap(const DicomMap& source,
+                                           Encoding defaultEncoding)
+  {
+    pimpl_->file_.reset(new DcmFileFormat);
+
+    const DicomValue* tmp = source.TestAndGetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET);
+
+    if (tmp == NULL)
+    {
+      SetEncoding(defaultEncoding);
+    }
+    else if (tmp->IsBinary())
+    {
+      LOG(ERROR) << "Invalid binary string in the SpecificCharacterSet (0008,0005) tag";
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else if (tmp->IsNull() ||
+             tmp->GetContent().empty())
+    {
+      SetEncoding(defaultEncoding);
+    }
+    else
+    {
+      Encoding encoding;
+
+      if (GetDicomEncoding(encoding, tmp->GetContent().c_str()))
+      {
+        SetEncoding(encoding);
+      }
+      else
+      {
+        LOG(ERROR) << "Unsupported value for the SpecificCharacterSet (0008,0005) tag: \""
+                   << tmp->GetContent() << "\"";        
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    for (DicomMap::Map::const_iterator 
+           it = source.map_.begin(); it != source.map_.end(); ++it)
+    {
+      if (it->first != DICOM_TAG_SPECIFIC_CHARACTER_SET &&
+          !it->second->IsNull())
+      {
+        ReplacePlainString(it->first, it->second->GetContent());
+      }
+    }
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(const DicomMap& map,
+                                   Encoding defaultEncoding) :
+    pimpl_(new PImpl)
+  {
+    CreateFromDicomMap(map, defaultEncoding);
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(const DicomMap& map) : 
+    pimpl_(new PImpl)
+  {
+    CreateFromDicomMap(map, GetDefaultDicomEncoding());
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(const void* content, 
+                                   size_t size) : pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(content, size));
+  }
+
+  ParsedDicomFile::ParsedDicomFile(const std::string& content) : pimpl_(new PImpl)
+  {
+    if (content.size() == 0)
+    {
+      pimpl_->file_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(NULL, 0));
+    }
+    else
+    {
+      pimpl_->file_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(&content[0], content.size()));
+    }
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(ParsedDicomFile& other,
+                                   bool keepSopInstanceUid) : 
+    pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(dynamic_cast<DcmFileFormat*>(other.pimpl_->file_->clone()));
+
+    if (!keepSopInstanceUid)
+    {
+      // Create a new instance-level identifier
+      ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance));
+    }
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(DcmDataset& dicom) : pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(new DcmFileFormat(&dicom));
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(DcmFileFormat& dicom) : pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(new DcmFileFormat(dicom));
+  }
+
+
+  DcmFileFormat& ParsedDicomFile::GetDcmtkObject() const
+  {
+    return *pimpl_->file_.get();
+  }
+
+
+  ParsedDicomFile* ParsedDicomFile::Clone(bool keepSopInstanceUid)
+  {
+    return new ParsedDicomFile(*this, keepSopInstanceUid);
+  }
+
+
+  bool ParsedDicomFile::EmbedContentInternal(const std::string& dataUriScheme)
+  {
+    std::string mime, content;
+    if (!Toolbox::DecodeDataUriScheme(mime, content, dataUriScheme))
+    {
+      return false;
+    }
+
+    Toolbox::ToLowerCase(mime);
+
+    if (mime == "image/png")
+    {
+#if ORTHANC_ENABLE_PNG == 1
+      EmbedImage(mime, content);
+#else
+      LOG(ERROR) << "Orthanc was compiled without support of PNG";
+      throw OrthancException(ErrorCode_NotImplemented);
+#endif
+    }
+    else if (mime == "image/jpeg")
+    {
+#if ORTHANC_ENABLE_JPEG == 1
+      EmbedImage(mime, content);
+#else
+      LOG(ERROR) << "Orthanc was compiled without support of JPEG";
+      throw OrthancException(ErrorCode_NotImplemented);
+#endif
+    }
+    else if (mime == "application/pdf")
+    {
+      EmbedPdf(content);
+    }
+    else
+    {
+      LOG(ERROR) << "Unsupported MIME type for the content of a new DICOM file: " << mime;
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    return true;
+  }
+
+
+  void ParsedDicomFile::EmbedContent(const std::string& dataUriScheme)
+  {
+    if (!EmbedContentInternal(dataUriScheme))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+#if (ORTHANC_ENABLE_JPEG == 1 &&                \
+     ORTHANC_ENABLE_PNG == 1)
+  void ParsedDicomFile::EmbedImage(const std::string& mime,
+                                   const std::string& content)
+  {
+    if (mime == "image/png")
+    {
+      PngReader reader;
+      reader.ReadFromMemory(content);
+      EmbedImage(reader);
+    }
+    else if (mime == "image/jpeg")
+    {
+      JpegReader reader;
+      reader.ReadFromMemory(content);
+      EmbedImage(reader);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+#endif
+
+
+  void ParsedDicomFile::EmbedImage(const ImageAccessor& accessor)
+  {
+    if (accessor.GetFormat() != PixelFormat_Grayscale8 &&
+        accessor.GetFormat() != PixelFormat_Grayscale16 &&
+        accessor.GetFormat() != PixelFormat_SignedGrayscale16 &&
+        accessor.GetFormat() != PixelFormat_RGB24 &&
+        accessor.GetFormat() != PixelFormat_RGBA32)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    InvalidateCache();
+
+    if (accessor.GetFormat() == PixelFormat_RGBA32)
+    {
+      LOG(WARNING) << "Getting rid of the alpha channel when embedding a RGBA image inside DICOM";
+    }
+
+    // http://dicomiseasy.blogspot.be/2012/08/chapter-12-pixel-data.html
+
+    Remove(DICOM_TAG_PIXEL_DATA);
+    ReplacePlainString(DICOM_TAG_COLUMNS, boost::lexical_cast<std::string>(accessor.GetWidth()));
+    ReplacePlainString(DICOM_TAG_ROWS, boost::lexical_cast<std::string>(accessor.GetHeight()));
+    ReplacePlainString(DICOM_TAG_SAMPLES_PER_PIXEL, "1");
+    ReplacePlainString(DICOM_TAG_NUMBER_OF_FRAMES, "1");
+
+    if (accessor.GetFormat() == PixelFormat_SignedGrayscale16)
+    {
+      ReplacePlainString(DICOM_TAG_PIXEL_REPRESENTATION, "1");
+    }
+    else
+    {
+      ReplacePlainString(DICOM_TAG_PIXEL_REPRESENTATION, "0");  // Unsigned pixels
+    }
+
+    ReplacePlainString(DICOM_TAG_PLANAR_CONFIGURATION, "0");  // Color channels are interleaved
+    ReplacePlainString(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
+
+    unsigned int bytesPerPixel = 0;
+
+    switch (accessor.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "8");
+        ReplacePlainString(DICOM_TAG_BITS_STORED, "8");
+        ReplacePlainString(DICOM_TAG_HIGH_BIT, "7");
+        bytesPerPixel = 1;
+        break;
+
+      case PixelFormat_RGB24:
+      case PixelFormat_RGBA32:
+        ReplacePlainString(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "RGB");
+        ReplacePlainString(DICOM_TAG_SAMPLES_PER_PIXEL, "3");
+        ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "8");
+        ReplacePlainString(DICOM_TAG_BITS_STORED, "8");
+        ReplacePlainString(DICOM_TAG_HIGH_BIT, "7");
+        bytesPerPixel = 3;
+        break;
+
+      case PixelFormat_Grayscale16:
+      case PixelFormat_SignedGrayscale16:
+        ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "16");
+        ReplacePlainString(DICOM_TAG_BITS_STORED, "16");
+        ReplacePlainString(DICOM_TAG_HIGH_BIT, "15");
+        bytesPerPixel = 2;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    assert(bytesPerPixel != 0);
+
+    DcmTag key(DICOM_TAG_PIXEL_DATA.GetGroup(), 
+               DICOM_TAG_PIXEL_DATA.GetElement());
+
+    std::auto_ptr<DcmPixelData> pixels(new DcmPixelData(key));
+
+    unsigned int pitch = accessor.GetWidth() * bytesPerPixel;
+    Uint8* target = NULL;
+    pixels->createUint8Array(accessor.GetHeight() * pitch, target);
+
+    for (unsigned int y = 0; y < accessor.GetHeight(); y++)
+    {
+      switch (accessor.GetFormat())
+      {
+        case PixelFormat_RGB24:
+        case PixelFormat_Grayscale8:
+        case PixelFormat_Grayscale16:
+        case PixelFormat_SignedGrayscale16:
+        {
+          memcpy(target, reinterpret_cast<const Uint8*>(accessor.GetConstRow(y)), pitch);
+          target += pitch;
+          break;
+        }
+
+        case PixelFormat_RGBA32:
+        {
+          // The alpha channel is not supported by the DICOM standard
+          const Uint8* source = reinterpret_cast<const Uint8*>(accessor.GetConstRow(y));
+          for (unsigned int x = 0; x < accessor.GetWidth(); x++, target += 3, source += 4)
+          {
+            target[0] = source[0];
+            target[1] = source[1];
+            target[2] = source[2];
+          }
+
+          break;
+        }
+          
+        default:
+          throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+
+    if (!pimpl_->file_->getDataset()->insert(pixels.release(), false, false).good())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }    
+  }
+
+  
+  Encoding ParsedDicomFile::GetEncoding() const
+  {
+    return FromDcmtkBridge::DetectEncoding(*pimpl_->file_->getDataset(),
+                                           GetDefaultDicomEncoding());
+  }
+
+
+  void ParsedDicomFile::SetEncoding(Encoding encoding)
+  {
+    if (encoding == Encoding_Windows1251)
+    {
+      // This Cyrillic codepage is not officially supported by the
+      // DICOM standard. Do not set the SpecificCharacterSet tag.
+      return;
+    }
+
+    std::string s = GetDicomSpecificCharacterSet(encoding);
+    ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, s);
+  }
+
+  void ParsedDicomFile::DatasetToJson(Json::Value& target, 
+                                      DicomToJsonFormat format,
+                                      DicomToJsonFlags flags,
+                                      unsigned int maxStringLength)
+  {
+    std::set<DicomTag> ignoreTagLength;
+    FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(),
+                                        format, flags, maxStringLength,
+                                        GetDefaultDicomEncoding(), ignoreTagLength);
+  }
+
+
+  void ParsedDicomFile::DatasetToJson(Json::Value& target, 
+                                      DicomToJsonFormat format,
+                                      DicomToJsonFlags flags,
+                                      unsigned int maxStringLength,
+                                      const std::set<DicomTag>& ignoreTagLength)
+  {
+    FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(),
+                                        format, flags, maxStringLength,
+                                        GetDefaultDicomEncoding(), ignoreTagLength);
+  }
+
+
+  void ParsedDicomFile::DatasetToJson(Json::Value& target,
+                                      const std::set<DicomTag>& ignoreTagLength)
+  {
+    FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), ignoreTagLength);
+  }
+
+
+  void ParsedDicomFile::DatasetToJson(Json::Value& target)
+  {
+    const std::set<DicomTag> ignoreTagLength;
+    FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), ignoreTagLength);
+  }
+
+
+  void ParsedDicomFile::HeaderToJson(Json::Value& target, 
+                                     DicomToJsonFormat format)
+  {
+    FromDcmtkBridge::ExtractHeaderAsJson(target, *pimpl_->file_->getMetaInfo(), format, DicomToJsonFlags_None, 0);
+  }
+
+
+  bool ParsedDicomFile::HasTag(const DicomTag& tag) const
+  {
+    DcmTag key(tag.GetGroup(), tag.GetElement());
+    return pimpl_->file_->getDataset()->tagExists(key);
+  }
+
+
+  void ParsedDicomFile::EmbedPdf(const std::string& pdf)
+  {
+    if (pdf.size() < 5 ||  // (*)
+        strncmp("%PDF-", pdf.c_str(), 5) != 0)
+    {
+      LOG(ERROR) << "Not a PDF file";
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    InvalidateCache();
+
+    ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, UID_EncapsulatedPDFStorage);
+    ReplacePlainString(FromDcmtkBridge::Convert(DCM_Modality), "OT");
+    ReplacePlainString(FromDcmtkBridge::Convert(DCM_ConversionType), "WSD");
+    ReplacePlainString(FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument), "application/pdf");
+    //ReplacePlainString(FromDcmtkBridge::Convert(DCM_SeriesNumber), "1");
+
+    std::auto_ptr<DcmPolymorphOBOW> element(new DcmPolymorphOBOW(DCM_EncapsulatedDocument));
+
+    size_t s = pdf.size();
+    if (s & 1)
+    {
+      // The size of the buffer must be even
+      s += 1;
+    }
+
+    Uint8* bytes = NULL;
+    OFCondition result = element->createUint8Array(s, bytes);
+    if (!result.good() || bytes == NULL)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+
+    // Blank pad byte (no access violation, as "pdf.size() >= 5" because of (*) )
+    bytes[s - 1] = 0;
+
+    memcpy(bytes, pdf.c_str(), pdf.size());
+      
+    DcmPolymorphOBOW* obj = element.release();
+    result = pimpl_->file_->getDataset()->insert(obj);
+
+    if (!result.good())
+    {
+      delete obj;
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+  }
+
+
+  bool ParsedDicomFile::ExtractPdf(std::string& pdf)
+  {
+    std::string sop, mime;
+    
+    if (!GetTagValue(sop, DICOM_TAG_SOP_CLASS_UID) ||
+        !GetTagValue(mime, FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument)) ||
+        sop != UID_EncapsulatedPDFStorage ||
+        mime != "application/pdf")
+    {
+      return false;
+    }
+
+    if (!GetTagValue(pdf, DICOM_TAG_ENCAPSULATED_DOCUMENT))
+    {
+      return false;
+    }
+
+    // Strip the possible pad byte at the end of file, because the
+    // encapsulated documents must always have an even length. The PDF
+    // format expects files to end with %%EOF followed by CR/LF. If
+    // the last character of the file is not a CR or LF, we assume it
+    // is a pad byte and remove it.
+    if (pdf.size() > 0)
+    {
+      char last = *pdf.rbegin();
+
+      if (last != 10 && last != 13)
+      {
+        pdf.resize(pdf.size() - 1);
+      }
+    }
+
+    return true;
+  }
+
+
+  ParsedDicomFile* ParsedDicomFile::CreateFromJson(const Json::Value& json,
+                                                   DicomFromJsonFlags flags)
+  {
+    const bool generateIdentifiers = (flags & DicomFromJsonFlags_GenerateIdentifiers) ? true : false;
+    const bool decodeDataUriScheme = (flags & DicomFromJsonFlags_DecodeDataUriScheme) ? true : false;
+
+    std::auto_ptr<ParsedDicomFile> result(new ParsedDicomFile(generateIdentifiers));
+    result->SetEncoding(FromDcmtkBridge::ExtractEncoding(json, GetDefaultDicomEncoding()));
+
+    const Json::Value::Members tags = json.getMemberNames();
+    
+    for (size_t i = 0; i < tags.size(); i++)
+    {
+      DicomTag tag = FromDcmtkBridge::ParseTag(tags[i]);
+      const Json::Value& value = json[tags[i]];
+
+      if (tag == DICOM_TAG_PIXEL_DATA ||
+          tag == DICOM_TAG_ENCAPSULATED_DOCUMENT)
+      {
+        if (value.type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_BadRequest);
+        }
+        else
+        {
+          result->EmbedContent(value.asString());
+        }
+      }
+      else if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+        result->Replace(tag, value, decodeDataUriScheme, DicomReplaceMode_InsertIfAbsent);
+      }
+    }
+
+    return result.release();
+  }
+
+
+  void ParsedDicomFile::GetRawFrame(std::string& target,
+                                    std::string& mime,
+                                    unsigned int frameId)
+  {
+    if (pimpl_->frameIndex_.get() == NULL)
+    {
+      pimpl_->frameIndex_.reset(new DicomFrameIndex(*pimpl_->file_));
+    }
+
+    pimpl_->frameIndex_->GetRawFrame(target, frameId);
+
+    E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getOriginalXfer();
+    switch (transferSyntax)
+    {
+      case EXS_JPEGProcess1:
+        mime = "image/jpeg";
+        break;
+       
+      case EXS_JPEG2000LosslessOnly:
+      case EXS_JPEG2000:
+        mime = "image/jp2";
+        break;
+
+      default:
+        mime = "application/octet-stream";
+        break;
+    }
+  }
+
+
+  void ParsedDicomFile::InvalidateCache()
+  {
+    pimpl_->frameIndex_.reset(NULL);
+  }
+
+
+  unsigned int ParsedDicomFile::GetFramesCount() const
+  {
+    return DicomFrameIndex::GetFramesCount(*pimpl_->file_);
+  }
+
+
+  void ParsedDicomFile::ChangeEncoding(Encoding target)
+  {
+    Encoding source = GetEncoding();
+
+    if (source != target)  // Avoid unnecessary conversion
+    {
+      ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, GetDicomSpecificCharacterSet(target));
+      FromDcmtkBridge::ChangeStringEncoding(*pimpl_->file_->getDataset(), source, target);
+    }
+  }
+
+
+  void ParsedDicomFile::ExtractDicomSummary(DicomMap& target) const
+  {
+    FromDcmtkBridge::ExtractDicomSummary(target, *pimpl_->file_->getDataset());
+  }
+
+
+  bool ParsedDicomFile::LookupTransferSyntax(std::string& result)
+  {
+    return FromDcmtkBridge::LookupTransferSyntax(result, *pimpl_->file_);
+  }
+
+
+  bool ParsedDicomFile::LookupPhotometricInterpretation(PhotometricInterpretation& result) const
+  {
+    DcmTagKey k(DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetGroup(),
+                DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetElement());
+
+    DcmDataset& dataset = *pimpl_->file_->getDataset();
+
+    const char *c = NULL;
+    if (dataset.findAndGetString(k, c).good() &&
+        c != NULL)
+    {
+      result = StringToPhotometricInterpretation(c);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  void ParsedDicomFile::Apply(ITagVisitor& visitor)
+  {
+    FromDcmtkBridge::Apply(*pimpl_->file_->getDataset(), visitor, GetDefaultDicomEncoding());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/ParsedDicomFile.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,234 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_JPEG)
+#  error Macro ORTHANC_ENABLE_JPEG must be defined to use this file
+#endif
+
+#if !defined(ORTHANC_ENABLE_PNG)
+#  error Macro ORTHANC_ENABLE_PNG must be defined to use this file
+#endif
+
+#if !defined(ORTHANC_ENABLE_CIVETWEB)
+#  error Macro ORTHANC_ENABLE_CIVETWEB must be defined to use this file
+#endif
+
+#if !defined(ORTHANC_ENABLE_MONGOOSE)
+#  error Macro ORTHANC_ENABLE_MONGOOSE must be defined to use this file
+#endif
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#include "ITagVisitor.h"
+#include "../DicomFormat/DicomInstanceHasher.h"
+#include "../Images/ImageAccessor.h"
+#include "../IDynamicObject.h"
+#include "../Toolbox.h"
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+#  include "../RestApi/RestApiOutput.h"
+#endif
+
+#include <boost/shared_ptr.hpp>
+
+
+class DcmDataset;
+class DcmFileFormat;
+
+namespace Orthanc
+{
+  class ParsedDicomFile : public IDynamicObject
+  {
+  private:
+    struct PImpl;
+    boost::shared_ptr<PImpl> pimpl_;
+
+    ParsedDicomFile(ParsedDicomFile& other,
+                    bool keepSopInstanceUid);
+
+    void CreateFromDicomMap(const DicomMap& source,
+                            Encoding defaultEncoding);
+
+    void RemovePrivateTagsInternal(const std::set<DicomTag>* toKeep);
+
+    void UpdateStorageUid(const DicomTag& tag,
+                          const std::string& value,
+                          bool decodeDataUriScheme);
+
+    void InvalidateCache();
+
+    bool EmbedContentInternal(const std::string& dataUriScheme);
+
+  public:
+    ParsedDicomFile(bool createIdentifiers);  // Create a minimal DICOM instance
+
+    ParsedDicomFile(const DicomMap& map,
+                    Encoding defaultEncoding);
+
+    ParsedDicomFile(const DicomMap& map);
+
+    ParsedDicomFile(const void* content,
+                    size_t size);
+
+    ParsedDicomFile(const std::string& content);
+
+    ParsedDicomFile(DcmDataset& dicom);
+
+    ParsedDicomFile(DcmFileFormat& dicom);
+
+    DcmFileFormat& GetDcmtkObject() const;
+
+    ParsedDicomFile* Clone(bool keepSopInstanceUid);
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+    void SendPathValue(RestApiOutput& output,
+                       const UriComponents& uri);
+
+    void Answer(RestApiOutput& output);
+#endif
+
+    void Remove(const DicomTag& tag);
+
+    // Replace the DICOM tag as a NULL/empty value (e.g. for anonymization)
+    void Clear(const DicomTag& tag,
+               bool onlyIfExists);
+
+    void Replace(const DicomTag& tag,
+                 const std::string& utf8Value,
+                 bool decodeDataUriScheme,
+                 DicomReplaceMode mode);
+
+    void Replace(const DicomTag& tag,
+                 const Json::Value& value,  // Assumed to be encoded with UTF-8
+                 bool decodeDataUriScheme,
+                 DicomReplaceMode mode);
+
+    void Insert(const DicomTag& tag,
+                const Json::Value& value,   // Assumed to be encoded with UTF-8
+                bool decodeDataUriScheme);
+
+    void ReplacePlainString(const DicomTag& tag,
+                            const std::string& utf8Value)
+    {
+      Replace(tag, utf8Value, false, DicomReplaceMode_InsertIfAbsent);
+    }
+
+    void RemovePrivateTags()
+    {
+      RemovePrivateTagsInternal(NULL);
+    }
+
+    void RemovePrivateTags(const std::set<DicomTag>& toKeep)
+    {
+      RemovePrivateTagsInternal(&toKeep);
+    }
+
+    // WARNING: This function handles the decoding of strings to UTF8
+    bool GetTagValue(std::string& value,
+                     const DicomTag& tag);
+
+    DicomInstanceHasher GetHasher();
+
+    void SaveToMemoryBuffer(std::string& buffer);
+
+#if ORTHANC_SANDBOXED == 0
+    void SaveToFile(const std::string& path);
+#endif
+
+    void EmbedContent(const std::string& dataUriScheme);
+
+    void EmbedImage(const ImageAccessor& accessor);
+
+#if (ORTHANC_ENABLE_JPEG == 1 &&  \
+     ORTHANC_ENABLE_PNG == 1)
+    void EmbedImage(const std::string& mime,
+                    const std::string& content);
+#endif
+
+    Encoding GetEncoding() const;
+
+    // WARNING: This function only sets the encoding, it will not
+    // convert the encoding of the tags. Use "ChangeEncoding()" if need be.
+    void SetEncoding(Encoding encoding);
+
+    void DatasetToJson(Json::Value& target, 
+                       DicomToJsonFormat format,
+                       DicomToJsonFlags flags,
+                       unsigned int maxStringLength);
+
+    void DatasetToJson(Json::Value& target, 
+                       DicomToJsonFormat format,
+                       DicomToJsonFlags flags,
+                       unsigned int maxStringLength,
+                       const std::set<DicomTag>& ignoreTagLength);
+      
+    // This version uses the default parameters for
+    // FileContentType_DicomAsJson
+    void DatasetToJson(Json::Value& target,
+                       const std::set<DicomTag>& ignoreTagLength);
+
+    void DatasetToJson(Json::Value& target);
+
+    void HeaderToJson(Json::Value& target, 
+                      DicomToJsonFormat format);
+
+    bool HasTag(const DicomTag& tag) const;
+
+    void EmbedPdf(const std::string& pdf);
+
+    bool ExtractPdf(std::string& pdf);
+
+    void GetRawFrame(std::string& target, // OUT
+                     std::string& mime,   // OUT
+                     unsigned int frameId);  // IN
+
+    unsigned int GetFramesCount() const;
+
+    static ParsedDicomFile* CreateFromJson(const Json::Value& value,
+                                           DicomFromJsonFlags flags);
+
+    void ChangeEncoding(Encoding target);
+
+    void ExtractDicomSummary(DicomMap& target) const;
+
+    bool LookupTransferSyntax(std::string& result);
+
+    bool LookupPhotometricInterpretation(PhotometricInterpretation& result) const;
+
+    void Apply(ITagVisitor& visitor);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/ToDcmtkBridge.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,149 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "ToDcmtkBridge.h"
+
+#include <memory>
+
+#include "../OrthancException.h"
+
+
+namespace Orthanc
+{
+  DcmEVR ToDcmtkBridge::Convert(ValueRepresentation vr)
+  {
+    switch (vr)
+    {
+      case ValueRepresentation_ApplicationEntity:
+        return EVR_AE;
+
+      case ValueRepresentation_AgeString:
+        return EVR_AS;
+
+      case ValueRepresentation_AttributeTag:
+        return EVR_AT;
+
+      case ValueRepresentation_CodeString:
+        return EVR_CS;
+
+      case ValueRepresentation_Date:
+        return EVR_DA;
+
+      case ValueRepresentation_DecimalString:
+        return EVR_DS;
+
+      case ValueRepresentation_DateTime:
+        return EVR_DT;
+
+      case ValueRepresentation_FloatingPointSingle:
+        return EVR_FL;
+
+      case ValueRepresentation_FloatingPointDouble:
+        return EVR_FD;
+
+      case ValueRepresentation_IntegerString:
+        return EVR_IS;
+
+      case ValueRepresentation_LongString:
+        return EVR_LO;
+
+      case ValueRepresentation_LongText:
+        return EVR_LT;
+
+      case ValueRepresentation_OtherByte:
+        return EVR_OB;
+
+        // Not supported as of DCMTK 3.6.0
+        /*case ValueRepresentation_OtherDouble:
+          return EVR_OD;*/
+
+      case ValueRepresentation_OtherFloat:
+        return EVR_OF;
+
+        // Not supported as of DCMTK 3.6.0
+        /*case ValueRepresentation_OtherLong:
+          return EVR_OL;*/
+
+      case ValueRepresentation_OtherWord:
+        return EVR_OW;
+
+      case ValueRepresentation_PersonName:
+        return EVR_PN;
+
+      case ValueRepresentation_ShortString:
+        return EVR_SH;
+
+      case ValueRepresentation_SignedLong:
+        return EVR_SL;
+
+      case ValueRepresentation_Sequence:
+        return EVR_SQ;
+
+      case ValueRepresentation_SignedShort:
+        return EVR_SS;
+
+      case ValueRepresentation_ShortText:
+        return EVR_ST;
+
+      case ValueRepresentation_Time:
+        return EVR_TM;
+
+        // Not supported as of DCMTK 3.6.0
+        /*case ValueRepresentation_UnlimitedCharacters:
+          return EVR_UC;*/
+
+      case ValueRepresentation_UniqueIdentifier:
+        return EVR_UI;
+
+      case ValueRepresentation_UnsignedLong:
+        return EVR_UL;
+
+      case ValueRepresentation_Unknown:
+        return EVR_UN;
+
+        // Not supported as of DCMTK 3.6.0
+        /*case ValueRepresentation_UniversalResource:
+          return EVR_UR;*/
+
+      case ValueRepresentation_UnsignedShort:
+        return EVR_US;
+
+      case ValueRepresentation_UnlimitedText:
+        return EVR_UT;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomParsing/ToDcmtkBridge.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_DCMTK != 1
+#  error The macro ORTHANC_ENABLE_DCMTK must be set to 1
+#endif
+
+#include "../DicomFormat/DicomMap.h"
+#include <dcmtk/dcmdata/dcdatset.h>
+
+namespace Orthanc
+{
+  class ToDcmtkBridge
+  {
+  public:
+    static DcmTagKey Convert(const DicomTag& tag)
+    {
+      return DcmTagKey(tag.GetGroup(), tag.GetElement());
+    }
+
+    static DcmEVR Convert(ValueRepresentation vr);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Endianness.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,220 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+
+/********************************************************************
+ ** LINUX-LIKE ARCHITECTURES
+ ********************************************************************/
+
+#if defined(__LSB_VERSION__)
+// Linux Standard Base (LSB) does not come with be16toh, be32toh, and
+// be64toh
+#  define ORTHANC_HAS_BUILTIN_BYTE_SWAP 0
+#  include <endian.h>
+#elif defined(__linux__) || defined(__EMSCRIPTEN__)
+#  define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1
+#  include <endian.h>
+#endif
+
+
+/********************************************************************
+ ** WINDOWS ARCHITECTURES
+ **
+ ** On Windows x86, "host" will always be little-endian ("le").
+ ********************************************************************/
+
+#if defined(_WIN32)
+#  if defined(_MSC_VER)
+//   Visual Studio - http://msdn.microsoft.com/en-us/library/a3140177.aspx
+#    define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1
+#    define be16toh(x) _byteswap_ushort(x)
+#    define be32toh(x) _byteswap_ulong(x)
+#    define be64toh(x) _byteswap_uint64(x)
+#  elif (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3))
+//   MinGW >= 4.3 - Use builtin intrinsic for byte swapping
+#    define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1
+#    define be16toh(x) __builtin_bswap16(x)
+#    define be32toh(x) __builtin_bswap32(x)
+#    define be64toh(x) __builtin_bswap64(x)
+#  else
+//   MinGW <= 4.2, we must manually implement the byte swapping (*)
+#    define ORTHANC_HAS_BUILTIN_BYTE_SWAP 0
+#    define be16toh(x) __orthanc_bswap16(x)
+#    define be32toh(x) __orthanc_bswap32(x)
+#    define be64toh(x) __orthanc_bswap64(x)
+#  endif
+
+#  define htobe16(x) be16toh(x)
+#  define htobe32(x) be32toh(x)
+#  define htobe64(x) be64toh(x)
+
+#  define htole16(x) x
+#  define htole32(x) x
+#  define htole64(x) x
+
+#  define le16toh(x) x
+#  define le32toh(x) x
+#  define le64toh(x) x
+#endif
+
+
+/********************************************************************
+ ** FREEBSD ARCHITECTURES
+ ********************************************************************/
+
+#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
+#  define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1
+#  include <arpa/inet.h>
+#endif
+
+
+/********************************************************************
+ ** OPENBSD ARCHITECTURES
+ ********************************************************************/
+
+#if defined(__OpenBSD__)
+#  define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1
+#  include <endian.h>
+#endif
+
+
+/********************************************************************
+ ** APPLE ARCHITECTURES (including OS X)
+ ********************************************************************/
+
+#if defined(__APPLE__)
+#  define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1
+#  include <libkern/OSByteOrder.h>
+#  define be16toh(x) OSSwapBigToHostInt16(x)
+#  define be32toh(x) OSSwapBigToHostInt32(x)
+#  define be64toh(x) OSSwapBigToHostInt64(x)
+
+#  define htobe16(x) OSSwapHostToBigInt16(x)
+#  define htobe32(x) OSSwapHostToBigInt32(x)
+#  define htobe64(x) OSSwapHostToBigInt64(x)
+
+#  define htole16(x) OSSwapHostToLittleInt16(x)
+#  define htole32(x) OSSwapHostToLittleInt32(x)
+#  define htole64(x) OSSwapHostToLittleInt64(x)
+
+#  define le16toh(x) OSSwapLittleToHostInt16(x)
+#  define le32toh(x) OSSwapLittleToHostInt32(x)
+#  define le64toh(x) OSSwapLittleToHostInt64(x)
+#endif
+
+
+/********************************************************************
+ ** PORTABLE (BUT SLOW) IMPLEMENTATION OF BYTE-SWAPPING
+ ********************************************************************/
+
+#if ORTHANC_HAS_BUILTIN_BYTE_SWAP != 1
+
+#include <stdint.h>
+
+static inline uint16_t __orthanc_bswap16(uint16_t a)
+{
+  /**
+   * Note that an alternative implementation was included in Orthanc
+   * 1.4.0 and 1.4.1:
+   * 
+   *  # hg log -p -r 2706
+   *
+   * This alternative implementation only hid an underlying problem
+   * with pointer alignment on some architectures, and was thus
+   * reverted. Check out issue #99:
+   * https://bitbucket.org/sjodogne/orthanc/issues/99
+   **/
+  return (a << 8) | (a >> 8);
+}
+
+static inline uint32_t __orthanc_bswap32(uint32_t a)
+{
+  const uint8_t* p = reinterpret_cast<const uint8_t*>(&a);
+  return (static_cast<uint32_t>(p[0]) << 24 |
+          static_cast<uint32_t>(p[1]) << 16 |
+          static_cast<uint32_t>(p[2]) << 8 |
+          static_cast<uint32_t>(p[3]));
+}
+
+static inline uint64_t __orthanc_bswap64(uint64_t a)
+{
+  const uint8_t* p = reinterpret_cast<const uint8_t*>(&a);
+  return (static_cast<uint64_t>(p[0]) << 56 |
+          static_cast<uint64_t>(p[1]) << 48 |
+          static_cast<uint64_t>(p[2]) << 40 |
+          static_cast<uint64_t>(p[3]) << 32 |
+          static_cast<uint64_t>(p[4]) << 24 |
+          static_cast<uint64_t>(p[5]) << 16 |
+          static_cast<uint64_t>(p[6]) << 8 |
+          static_cast<uint64_t>(p[7]));
+}
+
+#if defined(_WIN32)
+// Implemented above (*)
+#elif defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && defined(__BIG_ENDIAN)
+#  if __BYTE_ORDER == __LITTLE_ENDIAN
+#    define be16toh(x) __orthanc_bswap16(x)
+#    define be32toh(x) __orthanc_bswap32(x)
+#    define be64toh(x) __orthanc_bswap64(x)
+#    define htobe16(x) __orthanc_bswap16(x)
+#    define htobe32(x) __orthanc_bswap32(x)
+#    define htobe64(x) __orthanc_bswap64(x)
+#    define htole16(x) x
+#    define htole32(x) x
+#    define htole64(x) x
+#    define le16toh(x) x
+#    define le32toh(x) x
+#    define le64toh(x) x
+#  elif __BYTE_ORDER == __BIG_ENDIAN
+#    define be16toh(x) x
+#    define be32toh(x) x
+#    define be64toh(x) x
+#    define htobe16(x) x
+#    define htobe32(x) x
+#    define htobe64(x) x
+#    define htole16(x) __orthanc_bswap16(x)
+#    define htole32(x) __orthanc_bswap32(x)
+#    define htole64(x) __orthanc_bswap64(x)
+#    define le16toh(x) __orthanc_bswap16(x)
+#    define le32toh(x) __orthanc_bswap32(x)
+#    define le64toh(x) __orthanc_bswap64(x)
+#  else
+#    error Please support your platform here
+#  endif
+#else
+#  error Please support your platform here
+#endif
+
+#endif
--- a/Core/EnumerationDictionary.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/EnumerationDictionary.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -60,6 +61,11 @@
         stringToEnumeration_.clear();
       }
 
+      bool Contains(Enumeration value) const
+      {
+        return enumerationToString_.find(value) != enumerationToString_.end();
+      }
+
       void Add(Enumeration value, const std::string& str)
       {
         // Check if these values are free
@@ -80,10 +86,9 @@
       {
         try
         {
-          int value = boost::lexical_cast<int>(str);
-          return static_cast<Enumeration>(value);
+          return static_cast<Enumeration>(boost::lexical_cast<int>(str));
         }
-        catch (boost::bad_lexical_cast)
+        catch (boost::bad_lexical_cast&)
         {
         }
 
--- a/Core/Enumerations.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Enumerations.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -35,8 +36,11 @@
 
 #include "OrthancException.h"
 #include "Toolbox.h"
+#include "Logging.h"
 
+#include <boost/thread/mutex.hpp>
 #include <string.h>
+#include <cassert>
 
 namespace Orthanc
 {
@@ -62,7 +66,7 @@
         return "Parameter out of range";
 
       case ErrorCode_NotEnoughMemory:
-        return "Not enough memory";
+        return "The server hosting Orthanc is running out of memory";
 
       case ErrorCode_BadParameterType:
         return "Bad type for a parameter";
@@ -151,6 +155,18 @@
       case ErrorCode_EmptyRequest:
         return "The request is empty";
 
+      case ErrorCode_NotAcceptable:
+        return "Cannot send a response which is acceptable according to the Accept HTTP header";
+
+      case ErrorCode_NullPointer:
+        return "Cannot handle a NULL pointer";
+
+      case ErrorCode_DatabaseUnavailable:
+        return "The database is currently not available (probably a transient situation)";
+
+      case ErrorCode_CanceledJob:
+        return "This job was canceled";
+
       case ErrorCode_SQLiteNotOpened:
         return "SQLite: The database is not opened";
 
@@ -209,10 +225,10 @@
         return "The specified path does not point to a directory";
 
       case ErrorCode_HttpPortInUse:
-        return "The TCP port of the HTTP server is already in use";
+        return "The TCP port of the HTTP server is privileged or already in use";
 
       case ErrorCode_DicomPortInUse:
-        return "The TCP port of the DICOM server is already in use";
+        return "The TCP port of the DICOM server is privileged or already in use";
 
       case ErrorCode_BadHttpStatusInRest:
         return "This HTTP status is not allowed in a REST API";
@@ -322,6 +338,12 @@
       case ErrorCode_CannotOrderSlices:
         return "Unable to order the slices of the series";
 
+      case ErrorCode_NoWorklistHandler:
+        return "No request handler factory for DICOM C-Find Modality SCP";
+
+      case ErrorCode_AlreadyExistingTag:
+        return "Cannot override the value of a tag that already exists";
+
       default:
         if (error >= ErrorCode_START_PLUGINS)
         {
@@ -621,10 +643,10 @@
         return "RGB";
 
       case PhotometricInterpretation_Monochrome1:
-        return "Monochrome1";
+        return "MONOCHROME1";
 
       case PhotometricInterpretation_Monochrome2:
-        return "Monochrome2";
+        return "MONOCHROME2";
 
       case PhotometricInterpretation_ARGB:
         return "ARGB";
@@ -636,25 +658,25 @@
         return "HSV";
 
       case PhotometricInterpretation_Palette:
-        return "Palette color";
+        return "PALETTE COLOR";
 
       case PhotometricInterpretation_YBRFull:
-        return "YBR full";
+        return "YBR_FULL";
 
       case PhotometricInterpretation_YBRFull422:
-        return "YBR full 422";
+        return "YBR_FULL_422";
 
       case PhotometricInterpretation_YBRPartial420:
-        return "YBR partial 420"; 
+        return "YBR_PARTIAL_420"; 
 
       case PhotometricInterpretation_YBRPartial422:
-        return "YBR partial 422"; 
+        return "YBR_PARTIAL_422"; 
 
       case PhotometricInterpretation_YBR_ICT:
-        return "YBR ICT"; 
+        return "YBR_ICT"; 
 
       case PhotometricInterpretation_YBR_RCT:
-        return "YBR RCT"; 
+        return "YBR_RCT"; 
 
       case PhotometricInterpretation_Unknown:
         return "Unknown";
@@ -675,8 +697,8 @@
       case RequestOrigin_DicomProtocol:
         return "DicomProtocol";
 
-      case RequestOrigin_Http:
-        return "Http";
+      case RequestOrigin_RestApi:
+        return "RestApi";
 
       case RequestOrigin_Plugins:
         return "Plugins";
@@ -712,6 +734,290 @@
   }
 
 
+  const char* EnumerationToString(PixelFormat format)
+  {
+    switch (format)
+    {
+      case PixelFormat_RGB24:
+        return "RGB24";
+
+      case PixelFormat_RGBA32:
+        return "RGBA32";
+
+      case PixelFormat_BGRA32:
+        return "BGRA32";
+
+      case PixelFormat_Grayscale8:
+        return "Grayscale (unsigned 8bpp)";
+
+      case PixelFormat_Grayscale16:
+        return "Grayscale (unsigned 16bpp)";
+
+      case PixelFormat_SignedGrayscale16:
+        return "Grayscale (signed 16bpp)";
+
+      case PixelFormat_Float32:
+        return "Grayscale (float 32bpp)";
+
+      case PixelFormat_Grayscale32:
+        return "Grayscale (unsigned 32bpp)";
+
+      case PixelFormat_Grayscale64:
+        return "Grayscale (unsigned 64bpp)";
+
+      case PixelFormat_RGB48:
+        return "RGB48";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(ModalityManufacturer manufacturer)
+  {
+    switch (manufacturer)
+    {
+      case ModalityManufacturer_Generic:
+        return "Generic";
+
+      case ModalityManufacturer_GenericNoWildcardInDates:
+        return "GenericNoWildcardInDates";
+
+      case ModalityManufacturer_GenericNoUniversalWildcard:
+        return "GenericNoUniversalWildcard";
+
+      case ModalityManufacturer_StoreScp:
+        return "StoreScp";
+      
+      case ModalityManufacturer_ClearCanvas:
+        return "ClearCanvas";
+      
+      case ModalityManufacturer_Dcm4Chee:
+        return "Dcm4Chee";
+      
+      case ModalityManufacturer_Vitrea:
+        return "Vitrea";
+      
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(DicomRequestType type)
+  {
+    switch (type)
+    {
+      case DicomRequestType_Echo:
+        return "Echo";
+        break;
+
+      case DicomRequestType_Find:
+        return "Find";
+        break;
+
+      case DicomRequestType_Get:
+        return "Get";
+        break;
+
+      case DicomRequestType_Move:
+        return "Move";
+        break;
+
+      case DicomRequestType_Store:
+        return "Store";
+        break;
+
+      default: 
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(TransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      case TransferSyntax_Deflated:
+        return "Deflated";
+
+      case TransferSyntax_Jpeg:
+        return "JPEG";
+
+      case TransferSyntax_Jpeg2000:
+        return "JPEG2000";
+
+      case TransferSyntax_JpegLossless:
+        return "JPEG Lossless";
+
+      case TransferSyntax_Jpip:
+        return "JPIP";
+
+      case TransferSyntax_Mpeg2:
+        return "MPEG2";
+
+      case TransferSyntax_Rle:
+        return "RLE";
+
+      default: 
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(DicomVersion version)
+  {
+    switch (version)
+    {
+      case DicomVersion_2008:
+        return "2008";
+        break;
+
+      case DicomVersion_2017c:
+        return "2017c";
+        break;
+
+      default: 
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(ValueRepresentation vr)
+  {
+    switch (vr)
+    {
+      case ValueRepresentation_ApplicationEntity:     // AE
+        return "AE";
+
+      case ValueRepresentation_AgeString:             // AS
+        return "AS";
+
+      case ValueRepresentation_AttributeTag:          // AT (2 x uint16_t)
+        return "AT";
+
+      case ValueRepresentation_CodeString:            // CS
+        return "CS";
+
+      case ValueRepresentation_Date:                  // DA
+        return "DA";
+
+      case ValueRepresentation_DecimalString:         // DS
+        return "DS";
+
+      case ValueRepresentation_DateTime:              // DT
+        return "DT";
+
+      case ValueRepresentation_FloatingPointSingle:   // FL (float)
+        return "FL";
+
+      case ValueRepresentation_FloatingPointDouble:   // FD (double)
+        return "FD";
+
+      case ValueRepresentation_IntegerString:         // IS
+        return "IS";
+
+      case ValueRepresentation_LongString:            // LO
+        return "LO";
+
+      case ValueRepresentation_LongText:              // LT
+        return "LT";
+
+      case ValueRepresentation_OtherByte:             // OB
+        return "OB";
+
+      case ValueRepresentation_OtherDouble:           // OD
+        return "OD";
+
+      case ValueRepresentation_OtherFloat:            // OF
+        return "OF";
+
+      case ValueRepresentation_OtherLong:             // OL
+        return "OL";
+
+      case ValueRepresentation_OtherWord:             // OW
+        return "OW";
+
+      case ValueRepresentation_PersonName:            // PN
+        return "PN";
+
+      case ValueRepresentation_ShortString:           // SH
+        return "SH";
+
+      case ValueRepresentation_SignedLong:            // SL (int32_t)
+        return "SL";
+
+      case ValueRepresentation_Sequence:              // SQ
+        return "SQ";
+
+      case ValueRepresentation_SignedShort:           // SS (int16_t)
+        return "SS";
+
+      case ValueRepresentation_ShortText:             // ST
+        return "ST";
+
+      case ValueRepresentation_Time:                  // TM
+        return "TM";
+
+      case ValueRepresentation_UnlimitedCharacters:   // UC
+        return "UC";
+
+      case ValueRepresentation_UniqueIdentifier:      // UI (UID)
+        return "UI";
+
+      case ValueRepresentation_UnsignedLong:          // UL (uint32_t)
+        return "UL";
+
+      case ValueRepresentation_Unknown:               // UN
+        return "UN";
+
+      case ValueRepresentation_UniversalResource:     // UR (URI or URL)
+        return "UR";
+
+      case ValueRepresentation_UnsignedShort:         // US (uint16_t)
+        return "US";
+
+      case ValueRepresentation_UnlimitedText:         // UT
+        return "UT";
+
+      case ValueRepresentation_NotSupported:
+        return "Not supported";
+
+      default: 
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(JobState state)
+  {
+    switch (state)
+    {
+      case JobState_Pending:
+        return "Pending";
+        
+      case JobState_Running:
+        return "Running";
+        
+      case JobState_Success:
+        return "Success";
+        
+      case JobState_Failure:
+        return "Failure";
+        
+      case JobState_Paused:
+        return "Paused";
+        
+      case JobState_Retry:
+        return "Retry";
+        
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+  
+
   Encoding StringToEncoding(const char* encoding)
   {
     std::string s(encoding);
@@ -862,6 +1168,368 @@
   }
 
 
+  ValueRepresentation StringToValueRepresentation(const std::string& vr,
+                                                  bool throwIfUnsupported)
+  {
+    if (vr == "AE")
+    {
+      return ValueRepresentation_ApplicationEntity;
+    }
+    else if (vr == "AS")
+    {
+      return ValueRepresentation_AgeString;
+    }
+    else if (vr == "AT")
+    {
+      return ValueRepresentation_AttributeTag;
+    }
+    else if (vr == "CS")
+    {
+      return ValueRepresentation_CodeString;
+    }
+    else if (vr == "DA")
+    {
+      return ValueRepresentation_Date;
+    }
+    else if (vr == "DS")
+    {
+      return ValueRepresentation_DecimalString;
+    }
+    else if (vr == "DT")
+    {
+      return ValueRepresentation_DateTime;
+    }
+    else if (vr == "FL")
+    {
+      return ValueRepresentation_FloatingPointSingle;
+    }
+    else if (vr == "FD")
+    {
+      return ValueRepresentation_FloatingPointDouble;
+    }
+    else if (vr == "IS")
+    {
+      return ValueRepresentation_IntegerString;
+    }
+    else if (vr == "LO")
+    {
+      return ValueRepresentation_LongString;
+    }
+    else if (vr == "LT")
+    {
+      return ValueRepresentation_LongText;
+    }
+    else if (vr == "OB")
+    {
+      return ValueRepresentation_OtherByte;
+    }
+    else if (vr == "OD")
+    {
+      return ValueRepresentation_OtherDouble;
+    }
+    else if (vr == "OF")
+    {
+      return ValueRepresentation_OtherFloat;
+    }
+    else if (vr == "OL")
+    {
+      return ValueRepresentation_OtherLong;
+    }
+    else if (vr == "OW")
+    {
+      return ValueRepresentation_OtherWord;
+    }
+    else if (vr == "PN")
+    {
+      return ValueRepresentation_PersonName;
+    }
+    else if (vr == "SH")
+    {
+      return ValueRepresentation_ShortString;
+    }
+    else if (vr == "SL")
+    {
+      return ValueRepresentation_SignedLong;
+    }
+    else if (vr == "SQ")
+    {
+      return ValueRepresentation_Sequence;
+    }
+    else if (vr == "SS")
+    {
+      return ValueRepresentation_SignedShort;
+    }
+    else if (vr == "ST")
+    {
+      return ValueRepresentation_ShortText;
+    }
+    else if (vr == "TM")
+    {
+      return ValueRepresentation_Time;
+    }
+    else if (vr == "UC")
+    {
+      return ValueRepresentation_UnlimitedCharacters;
+    }
+    else if (vr == "UI")
+    {
+      return ValueRepresentation_UniqueIdentifier;
+    }
+    else if (vr == "UL")
+    {
+      return ValueRepresentation_UnsignedLong;
+    }
+    else if (vr == "UN")
+    {
+      return ValueRepresentation_Unknown;
+    }
+    else if (vr == "UR")
+    {
+      return ValueRepresentation_UniversalResource;
+    }
+    else if (vr == "US")
+    {
+      return ValueRepresentation_UnsignedShort;
+    }
+    else if (vr == "UT")
+    {
+      return ValueRepresentation_UnlimitedText;
+    }
+    else
+    {
+      std::string s = "Unsupported value representation encountered: " + vr;
+
+      if (throwIfUnsupported)
+      {
+        LOG(ERROR) << s;
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        LOG(INFO) << s;
+        return ValueRepresentation_NotSupported;
+      }
+    }
+  }
+
+
+  PhotometricInterpretation StringToPhotometricInterpretation(const char* value)
+  {
+    // http://dicom.nema.org/medical/dicom/2017a/output/chtml/part03/sect_C.7.6.3.html#sect_C.7.6.3.1.2
+    std::string s(value);
+
+    if (s == "MONOCHROME1")
+    {
+      return PhotometricInterpretation_Monochrome1;
+    }
+    
+    if (s == "MONOCHROME2")
+    {
+      return PhotometricInterpretation_Monochrome2;
+    }
+
+    if (s == "PALETTE COLOR")
+    {
+      return PhotometricInterpretation_Palette;
+    }
+    
+    if (s == "RGB")
+    {
+      return PhotometricInterpretation_RGB;
+    }
+    
+    if (s == "HSV")
+    {
+      return PhotometricInterpretation_HSV;
+    }
+    
+    if (s == "ARGB")
+    {
+      return PhotometricInterpretation_ARGB;
+    }    
+
+    if (s == "CMYK")
+    {
+      return PhotometricInterpretation_CMYK;
+    }    
+
+    if (s == "YBR_FULL")
+    {
+      return PhotometricInterpretation_YBRFull;
+    }
+    
+    if (s == "YBR_FULL_422")
+    {
+      return PhotometricInterpretation_YBRFull422;
+    }
+    
+    if (s == "YBR_PARTIAL_422")
+    {
+      return PhotometricInterpretation_YBRPartial422;
+    }
+    
+    if (s == "YBR_PARTIAL_420")
+    {
+      return PhotometricInterpretation_YBRPartial420;
+    }
+    
+    if (s == "YBR_ICT")
+    {
+      return PhotometricInterpretation_YBR_ICT;
+    }
+    
+    if (s == "YBR_RCT")
+    {
+      return PhotometricInterpretation_YBR_RCT;
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+  
+
+  ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer)
+  {
+    ModalityManufacturer result;
+    bool obsolete = false;
+    
+    if (manufacturer == "Generic")
+    {
+      return ModalityManufacturer_Generic;
+    }
+    else if (manufacturer == "GenericNoWildcardInDates")
+    {
+      return ModalityManufacturer_GenericNoWildcardInDates;
+    }
+    else if (manufacturer == "GenericNoUniversalWildcard")
+    {
+      return ModalityManufacturer_GenericNoUniversalWildcard;
+    }
+    else if (manufacturer == "ClearCanvas")
+    {
+      return ModalityManufacturer_ClearCanvas;
+    }
+    else if (manufacturer == "StoreScp")
+    {
+      return ModalityManufacturer_StoreScp;
+    }
+    else if (manufacturer == "Dcm4Chee")
+    {
+      return ModalityManufacturer_Dcm4Chee;
+    }
+    else if (manufacturer == "Vitrea")
+    {
+      return ModalityManufacturer_Vitrea;
+    }
+    else if (manufacturer == "AgfaImpax" ||
+             manufacturer == "SyngoVia")
+    {
+      result = ModalityManufacturer_GenericNoWildcardInDates;
+      obsolete = true;
+    }
+    else if (manufacturer == "EFilm2" ||
+             manufacturer == "MedInria")
+    {
+      result = ModalityManufacturer_Generic;
+      obsolete = true;
+    }
+    else
+    {
+      LOG(ERROR) << "Unknown modality manufacturer: \"" << manufacturer << "\"";
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (obsolete)
+    {
+      LOG(WARNING) << "The \"" << manufacturer << "\" manufacturer is obsolete since "
+                   << "Orthanc 1.3.0. To guarantee compatibility with future Orthanc "
+                   << "releases, you should replace it by \""
+                   << EnumerationToString(result)
+                   << "\" in your configuration file.";
+    }
+
+    return result;
+  }
+
+
+  DicomVersion StringToDicomVersion(const std::string& version)
+  {
+    if (version == "2008")
+    {
+      return DicomVersion_2008;
+    }
+    else if (version == "2017c")
+    {
+      return DicomVersion_2017c;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  JobState StringToJobState(const std::string& state)
+  {
+    if (state == "Pending")
+    {
+      return JobState_Pending;
+    }
+    else if (state == "Running")
+    {
+      return JobState_Running;
+    }
+    else if (state == "Success")
+    {
+      return JobState_Success;
+    }
+    else if (state == "Failure")
+    {
+      return JobState_Failure;
+    }
+    else if (state == "Paused")
+    {
+      return JobState_Paused;
+    }
+    else if (state == "Retry")
+    {
+      return JobState_Retry;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  RequestOrigin StringToRequestOrigin(const std::string& origin)
+  {
+    if (origin == "Unknown")
+    {
+      return RequestOrigin_Unknown;
+    }
+    else if (origin == "DicomProtocol")
+    {
+      return RequestOrigin_DicomProtocol;
+    }
+    else if (origin == "RestApi")
+    {
+      return RequestOrigin_RestApi;
+    }
+    else if (origin == "Plugins")
+    {
+      return RequestOrigin_Plugins;
+    }
+    else if (origin == "Lua")
+    {
+      return RequestOrigin_Lua;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+  
+
   unsigned int GetBytesPerPixel(PixelFormat format)
   {
     switch (format)
@@ -877,8 +1545,20 @@
         return 3;
 
       case PixelFormat_RGBA32:
+      case PixelFormat_BGRA32:
+      case PixelFormat_Grayscale32:
         return 4;
 
+      case PixelFormat_Float32:
+        assert(sizeof(float) == 4);
+        return 4;
+
+      case PixelFormat_RGB48:
+        return 6;
+
+      case PixelFormat_Grayscale64:
+        return 8;
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -888,15 +1568,18 @@
   bool GetDicomEncoding(Encoding& encoding,
                         const char* specificCharacterSet)
   {
-    std::string s = specificCharacterSet;
+    std::string s = Toolbox::StripSpaces(specificCharacterSet);
     Toolbox::ToUpperCase(s);
 
-    // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/
+    // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
     // https://github.com/dcm4che/dcm4che/blob/master/dcm4che-core/src/main/java/org/dcm4che3/data/SpecificCharacterSet.java
     if (s == "ISO_IR 6" ||
-        s == "ISO_IR 192" ||
         s == "ISO 2022 IR 6")
     {
+      encoding = Encoding_Ascii;
+    }
+    else if (s == "ISO_IR 192")
+    {
       encoding = Encoding_Utf8;
     }
     else if (s == "ISO_IR 100" ||
@@ -952,8 +1635,15 @@
     {
       encoding = Encoding_Japanese;
     }
-    else if (s == "GB18030")
+    else if (s == "GB18030" || s == "GBK")
     {
+      /**
+       * According to tumashu@163.com, "In China, many dicom file's
+       * 0008,0005 tag is set as "GBK", instead of "GB18030", GBK is a
+       * subset of GB18030, and which is used frequently in China,
+       * suggest support it."
+       * https://groups.google.com/d/msg/orthanc-users/WMM8LMbjpUc/02-1f_yFCgAJ
+       **/
       encoding = Encoding_Chinese;
     }
     /*
@@ -980,39 +1670,6 @@
   }
 
 
-  const char* GetMimeType(FileContentType type)
-  {
-    switch (type)
-    {
-      case FileContentType_Dicom:
-        return "application/dicom";
-
-      case FileContentType_DicomAsJson:
-        return "application/json";
-
-      default:
-        return "application/octet-stream";
-    }
-  }
-
-
-  const char* GetFileExtension(FileContentType type)
-  {
-    switch (type)
-    {
-      case FileContentType_Dicom:
-        return ".dcm";
-
-      case FileContentType_DicomAsJson:
-        return ".json";
-
-      default:
-        // Unknown file type
-        return "";
-    }
-  }
-
-
   ResourceType GetChildResourceType(ResourceType type)
   {
     switch (type)
@@ -1073,11 +1730,13 @@
 
   const char* GetDicomSpecificCharacterSet(Encoding encoding)
   {
-    // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/
+    // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
     switch (encoding)
     {
+      case Encoding_Ascii:
+        return "ISO_IR 6";
+
       case Encoding_Utf8:
-      case Encoding_Ascii:
         return "ISO_IR 192";
 
       case Encoding_Latin1:
@@ -1164,8 +1823,100 @@
       case ErrorCode_Unauthorized:
         return HttpStatus_401_Unauthorized;
 
+      case ErrorCode_NotAcceptable:
+        return HttpStatus_406_NotAcceptable;
+
+      case ErrorCode_DatabaseUnavailable:
+        return HttpStatus_503_ServiceUnavailable;
+
       default:
         return HttpStatus_500_InternalServerError;
     }
   }
+
+
+  bool IsUserContentType(FileContentType type)
+  {
+    return (type >= FileContentType_StartUser &&
+            type <= FileContentType_EndUser);
+  }
+
+
+  bool IsBinaryValueRepresentation(ValueRepresentation vr)
+  {
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
+
+    switch (vr)
+    {
+      case ValueRepresentation_ApplicationEntity:     // AE
+      case ValueRepresentation_AgeString:             // AS
+      case ValueRepresentation_CodeString:            // CS
+      case ValueRepresentation_Date:                  // DA
+      case ValueRepresentation_DecimalString:         // DS
+      case ValueRepresentation_DateTime:              // DT
+      case ValueRepresentation_IntegerString:         // IS
+      case ValueRepresentation_LongString:            // LO
+      case ValueRepresentation_LongText:              // LT
+      case ValueRepresentation_PersonName:            // PN
+      case ValueRepresentation_ShortString:           // SH
+      case ValueRepresentation_ShortText:             // ST
+      case ValueRepresentation_Time:                  // TM
+      case ValueRepresentation_UnlimitedCharacters:   // UC
+      case ValueRepresentation_UniqueIdentifier:      // UI (UID)
+      case ValueRepresentation_UniversalResource:     // UR (URI or URL)
+      case ValueRepresentation_UnlimitedText:         // UT
+      {
+        return false;
+      }
+
+      /**
+       * Below are all the VR whose character repertoire is tagged as
+       * "not applicable"
+       **/
+      case ValueRepresentation_AttributeTag:          // AT (2 x uint16_t)
+      case ValueRepresentation_FloatingPointSingle:   // FL (float)
+      case ValueRepresentation_FloatingPointDouble:   // FD (double)
+      case ValueRepresentation_OtherByte:             // OB
+      case ValueRepresentation_OtherDouble:           // OD
+      case ValueRepresentation_OtherFloat:            // OF
+      case ValueRepresentation_OtherLong:             // OL
+      case ValueRepresentation_OtherWord:             // OW
+      case ValueRepresentation_SignedLong:            // SL (int32_t)
+      case ValueRepresentation_Sequence:              // SQ
+      case ValueRepresentation_SignedShort:           // SS (int16_t)
+      case ValueRepresentation_UnsignedLong:          // UL (uint32_t)
+      case ValueRepresentation_Unknown:               // UN
+      case ValueRepresentation_UnsignedShort:         // US (uint16_t)
+      {
+        return true;
+      }
+
+      case ValueRepresentation_NotSupported:
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }  
+
+
+  static boost::mutex  defaultEncodingMutex_;  // Should not be necessary
+  static Encoding      defaultEncoding_ = ORTHANC_DEFAULT_DICOM_ENCODING;
+  
+  Encoding GetDefaultDicomEncoding()
+  {
+    boost::mutex::scoped_lock lock(defaultEncodingMutex_);
+    return defaultEncoding_;
+  }
+
+  void SetDefaultDicomEncoding(Encoding encoding)
+  {
+    std::string name = EnumerationToString(encoding);
+    
+    {
+      boost::mutex::scoped_lock lock(defaultEncodingMutex_);
+      defaultEncoding_ = encoding;
+    }
+
+    LOG(INFO) << "Default encoding for DICOM was changed to: " << name;
+  }
+
 }
--- a/Core/Enumerations.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Enumerations.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,6 +33,18 @@
 
 #pragma once
 
+#include <string>
+
+
+#if defined(_MSC_VER)
+#  define ORTHANC_FORCE_INLINE __forceinline
+#elif defined(__GNUC__) || defined(__clang__) || defined(__EMSCRIPTEN__)
+#  define ORTHANC_FORCE_INLINE inline __attribute((always_inline))
+#else
+#  error Please support your compiler here
+#endif
+
+
 namespace Orthanc
 {
   enum Endianness
@@ -50,7 +63,7 @@
     ErrorCode_Plugin = 1    /*!< Error encountered within the plugin engine */,
     ErrorCode_NotImplemented = 2    /*!< Not implemented yet */,
     ErrorCode_ParameterOutOfRange = 3    /*!< Parameter out of range */,
-    ErrorCode_NotEnoughMemory = 4    /*!< Not enough memory */,
+    ErrorCode_NotEnoughMemory = 4    /*!< The server hosting Orthanc is running out of memory */,
     ErrorCode_BadParameterType = 5    /*!< Bad type for a parameter */,
     ErrorCode_BadSequenceOfCalls = 6    /*!< Bad sequence of calls */,
     ErrorCode_InexistentItem = 7    /*!< Accessing an inexistent item */,
@@ -80,6 +93,10 @@
     ErrorCode_DatabasePlugin = 31    /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */,
     ErrorCode_StorageAreaPlugin = 32    /*!< Error in the plugin implementing a custom storage area */,
     ErrorCode_EmptyRequest = 33    /*!< The request is empty */,
+    ErrorCode_NotAcceptable = 34    /*!< Cannot send a response which is acceptable according to the Accept HTTP header */,
+    ErrorCode_NullPointer = 35    /*!< Cannot handle a NULL pointer */,
+    ErrorCode_DatabaseUnavailable = 36    /*!< The database is currently not available (probably a transient situation) */,
+    ErrorCode_CanceledJob = 37    /*!< This job was canceled */,
     ErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     ErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     ErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
@@ -99,8 +116,8 @@
     ErrorCode_DirectoryOverFile = 2000    /*!< The directory to be created is already occupied by a regular file */,
     ErrorCode_FileStorageCannotWrite = 2001    /*!< Unable to create a subdirectory or a file in the file storage */,
     ErrorCode_DirectoryExpected = 2002    /*!< The specified path does not point to a directory */,
-    ErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is already in use */,
-    ErrorCode_DicomPortInUse = 2004    /*!< The TCP port of the DICOM server is already in use */,
+    ErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is privileged or already in use */,
+    ErrorCode_DicomPortInUse = 2004    /*!< The TCP port of the DICOM server is privileged or already in use */,
     ErrorCode_BadHttpStatusInRest = 2005    /*!< This HTTP status is not allowed in a REST API */,
     ErrorCode_RegularFileExpected = 2006    /*!< The specified path does not point to a regular file */,
     ErrorCode_PathToExecutable = 2007    /*!< Unable to get the path to the executable */,
@@ -137,6 +154,8 @@
     ErrorCode_DatabaseNotInitialized = 2038    /*!< Plugin trying to call the database during its initialization */,
     ErrorCode_SslDisabled = 2039    /*!< Orthanc has been built without SSL support */,
     ErrorCode_CannotOrderSlices = 2040    /*!< Unable to order the slices of the series */,
+    ErrorCode_NoWorklistHandler = 2041    /*!< No request handler factory for DICOM C-Find Modality SCP */,
+    ErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
     ErrorCode_START_PLUGINS = 1000000
   };
 
@@ -184,7 +203,35 @@
      * {summary}{Graylevel, signed 16bpp image.}
      * {description}{The image is graylevel. Each pixel is signed and stored in two bytes.}
      **/
-    PixelFormat_SignedGrayscale16 = 5
+    PixelFormat_SignedGrayscale16 = 5,
+      
+    /**
+     * {summary}{Graylevel, floating-point image.}
+     * {description}{The image is graylevel. Each pixel is floating-point and stored in 4 bytes.}
+     **/
+    PixelFormat_Float32 = 6,
+
+    // This is the memory layout for Cairo (for internal use in Stone of Orthanc)
+    PixelFormat_BGRA32 = 7,
+
+    /**
+     * {summary}{Graylevel, unsigned 32bpp image.}
+     * {description}{The image is graylevel. Each pixel is unsigned and stored in 4 bytes.}
+     **/
+    PixelFormat_Grayscale32 = 8,
+    
+    /**
+     * {summary}{Color image in RGB48 format.}
+     * {description}{This format describes a color image. The pixels are stored in 6
+     * consecutive bytes. The memory layout is RGB.}
+     **/
+    PixelFormat_RGB48 = 9,
+
+    /**
+     * {summary}{Graylevel, unsigned 64bpp image.}
+     * {description}{The image is graylevel. Each pixel is unsigned and stored in 4 bytes.}
+     **/
+    PixelFormat_Grayscale64 = 10
   };
 
 
@@ -312,7 +359,8 @@
   };
 
 
-  // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/
+  // Specific Character Sets
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
   enum Encoding
   {
     Encoding_Ascii,
@@ -336,7 +384,7 @@
   };
 
 
-  // https://www.dabsoft.ch/dicom/3/C.7.6.3.1.2/
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.2
   enum PhotometricInterpretation
   {
     PhotometricInterpretation_ARGB,  // Retired
@@ -368,11 +416,169 @@
   {
     RequestOrigin_Unknown,
     RequestOrigin_DicomProtocol,
-    RequestOrigin_Http,
+    RequestOrigin_RestApi,
     RequestOrigin_Plugins,
     RequestOrigin_Lua
   };
 
+  enum ServerBarrierEvent
+  {
+    ServerBarrierEvent_Stop,
+    ServerBarrierEvent_Reload  // SIGHUP signal: reload configuration file
+  };
+
+  enum FileMode
+  {
+    FileMode_ReadBinary,
+    FileMode_WriteBinary
+  };
+
+  /**
+   * The value representations Orthanc knows about. They correspond to
+   * the DICOM 2016b version of the standard.
+   * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
+   **/
+  enum ValueRepresentation
+  {
+    ValueRepresentation_ApplicationEntity = 1,     // AE
+    ValueRepresentation_AgeString = 2,             // AS
+    ValueRepresentation_AttributeTag = 3,          // AT (2 x uint16_t)
+    ValueRepresentation_CodeString = 4,            // CS
+    ValueRepresentation_Date = 5,                  // DA
+    ValueRepresentation_DecimalString = 6,         // DS
+    ValueRepresentation_DateTime = 7,              // DT
+    ValueRepresentation_FloatingPointSingle = 8,   // FL (float)
+    ValueRepresentation_FloatingPointDouble = 9,   // FD (double)
+    ValueRepresentation_IntegerString = 10,        // IS
+    ValueRepresentation_LongString = 11,           // LO
+    ValueRepresentation_LongText = 12,             // LT
+    ValueRepresentation_OtherByte = 13,            // OB
+    ValueRepresentation_OtherDouble = 14,          // OD
+    ValueRepresentation_OtherFloat = 15,           // OF
+    ValueRepresentation_OtherLong = 16,            // OL
+    ValueRepresentation_OtherWord = 17,            // OW
+    ValueRepresentation_PersonName = 18,           // PN
+    ValueRepresentation_ShortString = 19,          // SH
+    ValueRepresentation_SignedLong = 20,           // SL (int32_t)
+    ValueRepresentation_Sequence = 21,             // SQ
+    ValueRepresentation_SignedShort = 22,          // SS (int16_t)
+    ValueRepresentation_ShortText = 23,            // ST
+    ValueRepresentation_Time = 24,                 // TM
+    ValueRepresentation_UnlimitedCharacters = 25,  // UC
+    ValueRepresentation_UniqueIdentifier = 26,     // UI (UID)
+    ValueRepresentation_UnsignedLong = 27,         // UL (uint32_t)
+    ValueRepresentation_Unknown = 28,              // UN
+    ValueRepresentation_UniversalResource = 29,    // UR (URI or URL)
+    ValueRepresentation_UnsignedShort = 30,        // US (uint16_t)
+    ValueRepresentation_UnlimitedText = 31,        // UT
+    ValueRepresentation_NotSupported               // Not supported by Orthanc, or tag not in dictionary
+  };
+
+  enum DicomReplaceMode
+  {
+    DicomReplaceMode_InsertIfAbsent,
+    DicomReplaceMode_ThrowIfAbsent,
+    DicomReplaceMode_IgnoreIfAbsent
+  };
+
+  enum DicomToJsonFormat
+  {
+    DicomToJsonFormat_Full,
+    DicomToJsonFormat_Short,
+    DicomToJsonFormat_Human
+  };
+
+  enum DicomToJsonFlags
+  {
+    DicomToJsonFlags_IncludeBinary         = (1 << 0),
+    DicomToJsonFlags_IncludePrivateTags    = (1 << 1),
+    DicomToJsonFlags_IncludeUnknownTags    = (1 << 2),
+    DicomToJsonFlags_IncludePixelData      = (1 << 3),
+    DicomToJsonFlags_ConvertBinaryToAscii  = (1 << 4),
+    DicomToJsonFlags_ConvertBinaryToNull   = (1 << 5),
+
+    // Some predefined combinations
+    DicomToJsonFlags_None     = 0,
+    DicomToJsonFlags_Default  = (DicomToJsonFlags_IncludeBinary |
+                                 DicomToJsonFlags_IncludePixelData | 
+                                 DicomToJsonFlags_IncludePrivateTags | 
+                                 DicomToJsonFlags_IncludeUnknownTags | 
+                                 DicomToJsonFlags_ConvertBinaryToNull)
+  };
+  
+  enum DicomFromJsonFlags
+  {
+    DicomFromJsonFlags_DecodeDataUriScheme = (1 << 0),
+    DicomFromJsonFlags_GenerateIdentifiers = (1 << 1),
+
+    // Some predefined combinations
+    DicomFromJsonFlags_None = 0
+  };
+  
+  enum DicomVersion
+  {
+    DicomVersion_2008,
+    DicomVersion_2017c
+  };
+
+  enum ModalityManufacturer
+  {
+    ModalityManufacturer_Generic,
+    ModalityManufacturer_GenericNoWildcardInDates,
+    ModalityManufacturer_GenericNoUniversalWildcard,
+    ModalityManufacturer_StoreScp,
+    ModalityManufacturer_ClearCanvas,
+    ModalityManufacturer_Dcm4Chee,
+    ModalityManufacturer_Vitrea
+  };
+
+  enum DicomRequestType
+  {
+    DicomRequestType_Echo,
+    DicomRequestType_Find,
+    DicomRequestType_Get,
+    DicomRequestType_Move,
+    DicomRequestType_Store
+  };
+
+  enum TransferSyntax
+  {
+    TransferSyntax_Deflated,
+    TransferSyntax_Jpeg,
+    TransferSyntax_Jpeg2000,
+    TransferSyntax_JpegLossless,
+    TransferSyntax_Jpip,
+    TransferSyntax_Mpeg2,
+    TransferSyntax_Rle
+  };
+
+  enum JobState
+  {
+    JobState_Pending,
+    JobState_Running,
+    JobState_Success,
+    JobState_Failure,
+    JobState_Paused,
+    JobState_Retry
+  };
+
+  enum JobStepCode
+  {
+    JobStepCode_Success,
+    JobStepCode_Failure,
+    JobStepCode_Continue,
+    JobStepCode_Retry
+  };
+
+  enum JobStopReason
+  {
+    JobStopReason_Paused,
+    JobStopReason_Canceled,
+    JobStopReason_Success,
+    JobStopReason_Failure,
+    JobStopReason_Retry
+  };
+
 
   /**
    * WARNING: Do not change the explicit values in the enumerations
@@ -441,23 +647,46 @@
 
   const char* EnumerationToString(RequestOrigin origin);
 
+  const char* EnumerationToString(PixelFormat format);
+
+  const char* EnumerationToString(ModalityManufacturer manufacturer);
+
+  const char* EnumerationToString(DicomRequestType type);
+
+  const char* EnumerationToString(TransferSyntax syntax);
+
+  const char* EnumerationToString(DicomVersion version);
+
+  const char* EnumerationToString(ValueRepresentation vr);
+
+  const char* EnumerationToString(JobState state);
+
   Encoding StringToEncoding(const char* encoding);
 
   ResourceType StringToResourceType(const char* type);
 
   ImageFormat StringToImageFormat(const char* format);
 
-  LogLevel StringToLogLevel(const char* format);
+  LogLevel StringToLogLevel(const char* level);
+
+  ValueRepresentation StringToValueRepresentation(const std::string& vr,
+                                                  bool throwIfUnsupported);
+
+  PhotometricInterpretation StringToPhotometricInterpretation(const char* value);
 
+  ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer);
+
+  DicomVersion StringToDicomVersion(const std::string& version);
+
+  JobState StringToJobState(const std::string& state);
+  
+  RequestOrigin StringToRequestOrigin(const std::string& origin);
+  
   unsigned int GetBytesPerPixel(PixelFormat format);
 
   bool GetDicomEncoding(Encoding& encoding,
                         const char* specificCharacterSet);
 
-  const char* GetMimeType(FileContentType type);
-
-  const char* GetFileExtension(FileContentType type);
-
   ResourceType GetChildResourceType(ResourceType type);
 
   ResourceType GetParentResourceType(ResourceType type);
@@ -467,4 +696,12 @@
   const char* GetDicomSpecificCharacterSet(Encoding encoding);
 
   HttpStatus ConvertErrorCodeToHttpStatus(ErrorCode error);
+
+  bool IsUserContentType(FileContentType type);
+
+  bool IsBinaryValueRepresentation(ValueRepresentation vr);
+  
+  Encoding GetDefaultDicomEncoding();
+
+  void SetDefaultDicomEncoding(Encoding encoding);
 }
--- a/Core/FileStorage/FileInfo.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/FileStorage/FileInfo.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/FileStorage/FilesystemStorage.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/FileStorage/FilesystemStorage.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -39,7 +40,7 @@
 #include "../Logging.h"
 #include "../OrthancException.h"
 #include "../Toolbox.h"
-#include "../Uuid.h"
+#include "../SystemToolbox.h"
 
 #include <boost/filesystem/fstream.hpp>
 
@@ -83,14 +84,40 @@
     //root_ = boost::filesystem::absolute(root).string();
     root_ = root;
 
-    Toolbox::MakeDirectory(root);
+    SystemToolbox::MakeDirectory(root);
   }
 
+
+
+  static const char* GetDescriptionInternal(FileContentType content)
+  {
+    // This function is for logging only (internal use), a more
+    // fully-featured version is available in ServerEnumerations.cpp
+    switch (content)
+    {
+      case FileContentType_Unknown:
+        return "Unknown";
+
+      case FileContentType_Dicom:
+        return "DICOM";
+
+      case FileContentType_DicomAsJson:
+        return "JSON summary of DICOM";
+
+      default:
+        return "User-defined";
+    }
+  }
+
+
   void FilesystemStorage::Create(const std::string& uuid,
                                  const void* content, 
                                  size_t size,
-                                 FileContentType /*type*/)
+                                 FileContentType type)
   {
+    LOG(INFO) << "Creating attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
+              << "\" type (size: " << (size / (1024 * 1024) + 1) << "MB)";
+
     boost::filesystem::path path;
     
     path = GetPath(uuid);
@@ -117,33 +144,19 @@
       }
     }
 
-    boost::filesystem::ofstream f;
-    f.open(path, std::ofstream::out | std::ios::binary);
-    if (!f.good())
-    {
-      throw OrthancException(ErrorCode_FileStorageCannotWrite);
-    }
-
-    if (size != 0)
-    {
-      f.write(static_cast<const char*>(content), size);
-      if (!f.good())
-      {
-        f.close();
-        throw OrthancException(ErrorCode_FileStorageCannotWrite);
-      }
-    }
-
-    f.close();
+    SystemToolbox::WriteFile(content, size, path.string());
   }
 
 
   void FilesystemStorage::Read(std::string& content,
                                const std::string& uuid,
-                               FileContentType /*type*/)
+                               FileContentType type)
   {
+    LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
+              << "\" content type";
+
     content.clear();
-    Toolbox::ReadFile(content, GetPath(uuid).string());
+    SystemToolbox::ReadFile(content, GetPath(uuid).string());
   }
 
 
@@ -165,7 +178,7 @@
     {
       for (fs::recursive_directory_iterator current(root_), end; current != end ; ++current)
       {
-        if (fs::is_regular_file(current->status()))
+        if (SystemToolbox::IsRegularFile(current->path().string()))
         {
           try
           {
@@ -186,7 +199,7 @@
               }
             }
           }
-          catch (fs::filesystem_error)
+          catch (fs::filesystem_error&)
           {
           }
         }
@@ -211,11 +224,9 @@
 
 
   void FilesystemStorage::Remove(const std::string& uuid,
-                                 FileContentType /*type*/)
+                                 FileContentType type)
   {
-#if ORTHANC_ENABLE_GOOGLE_LOG == 1
-    LOG(INFO) << "Deleting file " << uuid;
-#endif
+    LOG(INFO) << "Deleting attachment \"" << uuid << "\" of type " << static_cast<int>(type);
 
     namespace fs = boost::filesystem;
 
--- a/Core/FileStorage/FilesystemStorage.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/FileStorage/FilesystemStorage.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,6 +33,14 @@
 
 #pragma once
 
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The class FilesystemStorage cannot be used in sandboxed environments
+#endif
+
 #include "IStorageArea.h"
 
 #include <stdint.h>
@@ -52,7 +61,7 @@
     boost::filesystem::path GetPath(const std::string& uuid) const;
 
   public:
-    FilesystemStorage(std::string root);
+    explicit FilesystemStorage(std::string root);
 
     virtual void Create(const std::string& uuid,
                         const void* content, 
--- a/Core/FileStorage/IStorageArea.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/FileStorage/IStorageArea.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/FileStorage/MemoryStorageArea.cpp	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "MemoryStorageArea.h"
+
+#include "../OrthancException.h"
+#include "../Logging.h"
+
+namespace Orthanc
+{
+  MemoryStorageArea::~MemoryStorageArea()
+  {
+    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        delete it->second;
+      }
+    }
+  }
+    
+  void MemoryStorageArea::Create(const std::string& uuid,
+                                 const void* content,
+                                 size_t size,
+                                 FileContentType type)
+  {
+    LOG(INFO) << "Creating attachment \"" << uuid << "\" of \"" << static_cast<int>(type)
+              << "\" type (size: " << (size / (1024 * 1024) + 1) << "MB)";
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (size != 0 &&
+        content == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (content_.find(uuid) != content_.end())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      content_[uuid] = new std::string(reinterpret_cast<const char*>(content), size);
+    }
+  }
+
+  
+  void MemoryStorageArea::Read(std::string& content,
+                               const std::string& uuid,
+                               FileContentType type)
+  {
+    LOG(INFO) << "Reading attachment \"" << uuid << "\" of \""
+              << static_cast<int>(type) << "\" content type";
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Content::const_iterator found = content_.find(uuid);
+
+    if (found == content_.end())
+    {
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+    else if (found->second == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      content.assign(*found->second);
+    }
+  }
+      
+
+  void MemoryStorageArea::Remove(const std::string& uuid,
+                                 FileContentType type)
+  {
+    LOG(INFO) << "Deleting attachment \"" << uuid << "\" of type " << static_cast<int>(type);
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Content::iterator found = content_.find(uuid);
+    
+    if (found == content_.end())
+    {
+      // Ignore second removal
+    }
+    else if (found->second == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      delete found->second;
+      content_.erase(found);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/FileStorage/MemoryStorageArea.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IStorageArea.h"
+
+#include <boost/thread/mutex.hpp>
+#include <map>
+
+namespace Orthanc
+{
+  class MemoryStorageArea : public IStorageArea
+  {
+  private:
+    typedef std::map<std::string, std::string*>  Content;
+    
+    boost::mutex  mutex_;
+    Content       content_;
+    
+  public:
+    virtual ~MemoryStorageArea();
+    
+    virtual void Create(const std::string& uuid,
+                        const void* content,
+                        size_t size,
+                        FileContentType type);
+
+    virtual void Read(std::string& content,
+                      const std::string& uuid,
+                      FileContentType type);
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type);
+  };
+}
--- a/Core/FileStorage/StorageAccessor.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/FileStorage/StorageAccessor.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -35,7 +36,12 @@
 
 #include "../Compression/ZlibCompressor.h"
 #include "../OrthancException.h"
-#include "../HttpServer/HttpStreamTranscoder.h"
+#include "../Toolbox.h"
+#include "../Toolbox.h"
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+#  include "../HttpServer/HttpStreamTranscoder.h"
+#endif
 
 namespace Orthanc
 {
@@ -140,33 +146,59 @@
   }
 
 
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
   void StorageAccessor::SetupSender(BufferHttpSender& sender,
-                                    const FileInfo& info)
+                                    const FileInfo& info,
+                                    const std::string& mime)
   {
     area_.Read(sender.GetBuffer(), info.GetUuid(), info.GetContentType());
-    sender.SetContentType(GetMimeType(info.GetContentType()));
-    sender.SetContentFilename(info.GetUuid() + std::string(GetFileExtension(info.GetContentType())));
+    sender.SetContentType(mime);
+
+    const char* extension;
+    switch (info.GetContentType())
+    {
+      case FileContentType_Dicom:
+        extension = ".dcm";
+        break;
+
+      case FileContentType_DicomAsJson:
+        extension = ".json";
+        break;
+
+      default:
+        // Non-standard content type
+        extension = "";
+    }
+
+    sender.SetContentFilename(info.GetUuid() + std::string(extension));
   }
+#endif
 
 
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
   void StorageAccessor::AnswerFile(HttpOutput& output,
-                                   const FileInfo& info)
+                                   const FileInfo& info,
+                                   const std::string& mime)
   {
     BufferHttpSender sender;
-    SetupSender(sender, info);
+    SetupSender(sender, info, mime);
   
     HttpStreamTranscoder transcoder(sender, info.GetCompressionType());
     output.Answer(transcoder);
   }
+#endif
 
 
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
   void StorageAccessor::AnswerFile(RestApiOutput& output,
-                                   const FileInfo& info)
+                                   const FileInfo& info,
+                                   const std::string& mime)
   {
     BufferHttpSender sender;
-    SetupSender(sender, info);
+    SetupSender(sender, info, mime);
   
     HttpStreamTranscoder transcoder(sender, info.GetCompressionType());
     output.AnswerStream(transcoder);
   }
+#endif
 }
--- a/Core/FileStorage/StorageAccessor.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/FileStorage/StorageAccessor.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,10 +33,29 @@
 
 #pragma once
 
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The class StorageAccessor cannot be used in sandboxed environments
+#endif
+
+#if !defined(ORTHANC_ENABLE_CIVETWEB)
+#  error Macro ORTHANC_ENABLE_CIVETWEB must be defined to use this file
+#endif
+
+#if !defined(ORTHANC_ENABLE_MONGOOSE)
+#  error Macro ORTHANC_ENABLE_MONGOOSE must be defined to use this file
+#endif
+
 #include "IStorageArea.h"
 #include "FileInfo.h"
-#include "../HttpServer/BufferHttpSender.h"
-#include "../RestApi/RestApiOutput.h"
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+#  include "../HttpServer/BufferHttpSender.h"
+#  include "../RestApi/RestApiOutput.h"
+#endif
 
 #include <vector>
 #include <string>
@@ -50,8 +70,11 @@
   private:
     IStorageArea&  area_;
 
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
     void SetupSender(BufferHttpSender& sender,
-                     const FileInfo& info);
+                     const FileInfo& info,
+                     const std::string& mime);
+#endif
 
   public:
     StorageAccessor(IStorageArea& area) : area_(area)
@@ -84,10 +107,14 @@
       area_.Remove(info.GetUuid(), info.GetContentType());
     }
 
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
     void AnswerFile(HttpOutput& output,
-                    const FileInfo& info);
+                    const FileInfo& info,
+                    const std::string& mime);
 
     void AnswerFile(RestApiOutput& output,
-                    const FileInfo& info);
+                    const FileInfo& info,
+                    const std::string& mime);
+#endif
   };
 }
--- a/Core/HttpClient.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpClient.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -36,15 +37,19 @@
 #include "Toolbox.h"
 #include "OrthancException.h"
 #include "Logging.h"
+#include "ChunkedBuffer.h"
+#include "SystemToolbox.h"
 
 #include <string.h>
 #include <curl/curl.h>
 #include <boost/algorithm/string/predicate.hpp>
+#include <boost/thread/mutex.hpp>
 
 
-static std::string globalCACertificates_;
-static bool globalVerifyPeers_ = true;
-static long globalTimeout_ = 0;
+#if ORTHANC_ENABLE_PKCS11 == 1
+#  include "Pkcs11.h"
+#endif
+
 
 extern "C"
 {
@@ -69,7 +74,12 @@
 #endif
     static CURLcode OrthancHttpClientPerformSSL(CURL* curl, long* status)
   {
+#if ORTHANC_ENABLE_SSL == 1
     return GetHttpStatus(curl_easy_perform(curl), curl, status);
+#else
+    LOG(ERROR) << "Orthanc was compiled without SSL support, cannot make HTTPS request";
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+#endif
   }
 }
 
@@ -77,14 +87,116 @@
 
 namespace Orthanc
 {
+  class HttpClient::GlobalParameters
+  {
+  private:
+    boost::mutex    mutex_;
+    bool            httpsVerifyPeers_;
+    std::string     httpsCACertificates_;
+    std::string     proxy_;
+    long            timeout_;
+    bool            verbose_;
+
+    GlobalParameters() : 
+      httpsVerifyPeers_(true),
+      timeout_(0),
+      verbose_(false)
+    {
+    }
+
+  public:
+    // Singleton pattern
+    static GlobalParameters& GetInstance()
+    {
+      static GlobalParameters parameters;
+      return parameters;
+    }
+
+    void ConfigureSsl(bool httpsVerifyPeers,
+                      const std::string& httpsCACertificates)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      httpsVerifyPeers_ = httpsVerifyPeers;
+      httpsCACertificates_ = httpsCACertificates;
+    }
+
+    void GetSslConfiguration(bool& httpsVerifyPeers,
+                             std::string& httpsCACertificates)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      httpsVerifyPeers = httpsVerifyPeers_;
+      httpsCACertificates = httpsCACertificates_;
+    }
+
+    void SetDefaultProxy(const std::string& proxy)
+    {
+      LOG(INFO) << "Setting the default proxy for HTTP client connections: " << proxy;
+
+      {
+        boost::mutex::scoped_lock lock(mutex_);
+        proxy_ = proxy;
+      }
+    }
+
+    void GetDefaultProxy(std::string& target)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      target = proxy_;
+    }
+
+    void SetDefaultTimeout(long seconds)
+    {
+      LOG(INFO) << "Setting the default timeout for HTTP client connections: " << seconds << " seconds";
+
+      {
+        boost::mutex::scoped_lock lock(mutex_);
+        timeout_ = seconds;
+      }
+    }
+
+    long GetDefaultTimeout()
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      return timeout_;
+    }
+
+#if ORTHANC_ENABLE_PKCS11 == 1
+    bool IsPkcs11Initialized()
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      return Pkcs11::IsInitialized();
+    }
+
+    void InitializePkcs11(const std::string& module,
+                          const std::string& pin,
+                          bool verbose)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      Pkcs11::Initialize(module, pin, verbose);
+    }
+#endif
+
+    bool IsDefaultVerbose() const
+    {
+      return verbose_;
+    }
+
+    void SetDefaultVerbose(bool verbose) 
+    {
+      verbose_ = verbose;
+    }
+  };
+
+
   struct HttpClient::PImpl
   {
     CURL* curl_;
-    struct curl_slist *postHeaders_;
+    struct curl_slist *defaultPostHeaders_;
+    struct curl_slist *userHeaders_;
   };
 
 
-  static void ThrowException(HttpStatus status)
+  void HttpClient::ThrowException(HttpStatus status)
   {
     switch (status)
     {
@@ -92,10 +204,11 @@
         throw OrthancException(ErrorCode_BadRequest);
 
       case HttpStatus_401_Unauthorized:
+      case HttpStatus_403_Forbidden:
         throw OrthancException(ErrorCode_Unauthorized);
 
       case HttpStatus_404_NotFound:
-        throw OrthancException(ErrorCode_InexistentItem);
+        throw OrthancException(ErrorCode_UnknownResource);
 
       default:
         throw OrthancException(ErrorCode_NetworkProtocol);
@@ -103,9 +216,15 @@
   }
 
 
-
   static CURLcode CheckCode(CURLcode code)
   {
+    if (code == CURLE_NOT_BUILT_IN)
+    {
+      LOG(ERROR) << "Your libcurl does not contain a required feature, "
+                 << "please recompile Orthanc with -DUSE_SYSTEM_CURL=OFF";
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
     if (code != CURLE_OK)
     {
       LOG(ERROR) << "libCURL error: " + std::string(curl_easy_strerror(code));
@@ -116,27 +235,104 @@
   }
 
 
-  static size_t CurlCallback(void *buffer, size_t size, size_t nmemb, void *payload)
+  static size_t CurlBodyCallback(void *buffer, size_t size, size_t nmemb, void *payload)
   {
-    std::string& target = *(static_cast<std::string*>(payload));
+    ChunkedBuffer& target = *(static_cast<ChunkedBuffer*>(payload));
 
     size_t length = size * nmemb;
     if (length == 0)
+    {
       return 0;
+    }
+    else
+    {
+      target.AddChunk(buffer, length);
+      return length;
+    }
+  }
+
 
-    size_t pos = target.size();
+  /*static int CurlDebugCallback(CURL *handle,
+                               curl_infotype type,
+                               char *data,
+                               size_t size,
+                               void *userptr)
+  {
+    switch (type)
+    {
+      case CURLINFO_TEXT:
+      case CURLINFO_HEADER_IN:
+      case CURLINFO_HEADER_OUT:
+      case CURLINFO_SSL_DATA_IN:
+      case CURLINFO_SSL_DATA_OUT:
+      case CURLINFO_END:
+      case CURLINFO_DATA_IN:
+      case CURLINFO_DATA_OUT:
+      {
+        std::string s(data, size);
+        LOG(INFO) << "libcurl: " << s;
+        break;
+      }
+
+      default:
+        break;
+    }
+
+    return 0;
+    }*/
+
 
-    target.resize(pos + length);
-    memcpy(&target.at(pos), buffer, length);
+  struct CurlHeaderParameters
+  {
+    bool lowerCase_;
+    HttpClient::HttpHeaders* headers_;
+  };
+
+
+  static size_t CurlHeaderCallback(void *buffer, size_t size, size_t nmemb, void *payload)
+  {
+    CurlHeaderParameters& parameters = *(static_cast<CurlHeaderParameters*>(payload));
+    assert(parameters.headers_ != NULL);
 
-    return length;
+    size_t length = size * nmemb;
+    if (length == 0)
+    {
+      return 0;
+    }
+    else
+    {
+      std::string s(reinterpret_cast<const char*>(buffer), length);
+      std::size_t colon = s.find(':');
+      std::size_t eol = s.find("\r\n");
+      if (colon != std::string::npos &&
+          eol != std::string::npos)
+      {
+        std::string tmp(s.substr(0, colon));
+
+        if (parameters.lowerCase_)
+        {
+          Toolbox::ToLowerCase(tmp);
+        }
+
+        std::string key = Toolbox::StripSpaces(tmp);
+
+        if (!key.empty())
+        {
+          std::string value = Toolbox::StripSpaces(s.substr(colon + 1, eol));
+          (*parameters.headers_) [key] = value;
+        }
+      }
+
+      return length;
+    }
   }
 
 
   void HttpClient::Setup()
   {
-    pimpl_->postHeaders_ = NULL;
-    if ((pimpl_->postHeaders_ = curl_slist_append(pimpl_->postHeaders_, "Expect:")) == NULL)
+    pimpl_->userHeaders_ = NULL;
+    pimpl_->defaultPostHeaders_ = NULL;
+    if ((pimpl_->defaultPostHeaders_ = curl_slist_append(pimpl_->defaultPostHeaders_, "Expect:")) == NULL)
     {
       throw OrthancException(ErrorCode_NotEnoughMemory);
     }
@@ -144,11 +340,11 @@
     pimpl_->curl_ = curl_easy_init();
     if (!pimpl_->curl_)
     {
-      curl_slist_free_all(pimpl_->postHeaders_);
+      curl_slist_free_all(pimpl_->defaultPostHeaders_);
       throw OrthancException(ErrorCode_NotEnoughMemory);
     }
 
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEFUNCTION, &CurlCallback));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEFUNCTION, &CurlBodyCallback));
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADER, 0));
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1));
 
@@ -160,30 +356,56 @@
     url_ = "";
     method_ = HttpMethod_Get;
     lastStatus_ = HttpStatus_200_Ok;
-    isVerbose_ = false;
-    timeout_ = globalTimeout_;
-    verifyPeers_ = globalVerifyPeers_;
+    SetVerbose(GlobalParameters::GetInstance().IsDefaultVerbose());
+    timeout_ = GlobalParameters::GetInstance().GetDefaultTimeout();
+    GlobalParameters::GetInstance().GetDefaultProxy(proxy_);
+    GlobalParameters::GetInstance().GetSslConfiguration(verifyPeers_, caCertificates_);    
   }
 
 
-  HttpClient::HttpClient() : pimpl_(new PImpl)
+  HttpClient::HttpClient() : 
+    pimpl_(new PImpl), 
+    verifyPeers_(true),
+    pkcs11Enabled_(false),
+    headersToLowerCase_(true),
+    redirectionFollowed_(true)
   {
     Setup();
   }
 
 
-  HttpClient::HttpClient(const HttpClient& other) : pimpl_(new PImpl)
+  HttpClient::HttpClient(const WebServiceParameters& service,
+                         const std::string& uri) : 
+    pimpl_(new PImpl), 
+    verifyPeers_(true),
+    headersToLowerCase_(true),
+    redirectionFollowed_(true)
   {
     Setup();
 
-    if (other.IsVerbose())
+    if (service.GetUsername().size() != 0 && 
+        service.GetPassword().size() != 0)
     {
-      SetVerbose(true);
+      SetCredentials(service.GetUsername().c_str(), 
+                     service.GetPassword().c_str());
     }
 
-    if (other.credentials_.size() != 0)
+    if (!service.GetCertificateFile().empty())
     {
-      credentials_ = other.credentials_;
+      SetClientCertificate(service.GetCertificateFile(),
+                           service.GetCertificateKeyFile(),
+                           service.GetCertificateKeyPassword());
+    }
+
+    SetPkcs11Enabled(service.IsPkcs11Enabled());
+
+    SetUrl(service.GetUrl() + uri);
+
+    for (WebServiceParameters::Dictionary::const_iterator 
+           it = service.GetHttpHeaders().begin();
+         it != service.GetHttpHeaders().end(); ++it)
+    {
+      AddHeader(it->first, it->second);
     }
   }
 
@@ -191,7 +413,8 @@
   HttpClient::~HttpClient()
   {
     curl_easy_cleanup(pimpl_->curl_);
-    curl_slist_free_all(pimpl_->postHeaders_);
+    curl_slist_free_all(pimpl_->defaultPostHeaders_);
+    ClearHeaders();
   }
 
 
@@ -202,6 +425,7 @@
     if (isVerbose_)
     {
       CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_VERBOSE, 1));
+      //CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_DEBUGFUNCTION, &CurlDebugCallback));
     }
     else
     {
@@ -210,35 +434,140 @@
   }
 
 
-  bool HttpClient::Apply(std::string& answer)
+  void HttpClient::AddHeader(const std::string& key,
+                             const std::string& value)
   {
-    answer.clear();
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_URL, url_.c_str()));
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &answer));
+    if (key.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    std::string s = key + ": " + value;
+
+    if ((pimpl_->userHeaders_ = curl_slist_append(pimpl_->userHeaders_, s.c_str())) == NULL)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+  }
+
+
+  void HttpClient::ClearHeaders()
+  {
+    if (pimpl_->userHeaders_ != NULL)
+    {
+      curl_slist_free_all(pimpl_->userHeaders_);
+      pimpl_->userHeaders_ = NULL;
+    }
+  }
+
 
+  bool HttpClient::ApplyInternal(std::string& answerBody,
+                                 HttpHeaders* answerHeaders)
+  {
+    answerBody.clear();
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_URL, url_.c_str()));
+
+    CurlHeaderParameters headerParameters;
+
+    if (answerHeaders == NULL)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERFUNCTION, NULL));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERDATA, NULL));
+    }
+    else
+    {
+      headerParameters.lowerCase_ = headersToLowerCase_;
+      headerParameters.headers_ = answerHeaders;
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERFUNCTION, &CurlHeaderCallback));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERDATA, &headerParameters));
+    }
+
+#if ORTHANC_ENABLE_SSL == 1
     // Setup HTTPS-related options
-#if ORTHANC_SSL_ENABLED == 1
-    if (IsHttpsVerifyPeers())
+
+    if (verifyPeers_)
     {
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CAINFO, GetHttpsCACertificates().c_str()));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CAINFO, caCertificates_.c_str()));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYHOST, 2));  // libcurl default is strict verifyhost
       CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 1)); 
     }
     else
     {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYHOST, 0)); 
       CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 0)); 
     }
 #endif
 
+    // Setup the HTTPS client certificate
+    if (!clientCertificateFile_.empty() &&
+        pkcs11Enabled_)
+    {
+      LOG(ERROR) << "Cannot enable both client certificates and PKCS#11 authentication";
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (pkcs11Enabled_)
+    {
+#if ORTHANC_ENABLE_PKCS11 == 1
+      if (GlobalParameters::GetInstance().IsPkcs11Initialized())
+      {
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLENGINE, Pkcs11::GetEngineIdentifier()));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEYTYPE, "ENG"));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERTTYPE, "ENG"));
+      }
+      else
+      {
+        LOG(ERROR) << "Cannot use PKCS#11 for a HTTPS request, because it has not been initialized";
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+#else
+      LOG(ERROR) << "This version of Orthanc is compiled without support for PKCS#11";
+      throw OrthancException(ErrorCode_InternalError);
+#endif
+    }
+    else if (!clientCertificateFile_.empty())
+    {
+#if ORTHANC_ENABLE_SSL == 1
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERTTYPE, "PEM"));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERT, clientCertificateFile_.c_str()));
+
+      if (!clientCertificateKeyPassword_.empty())
+      {
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_KEYPASSWD, clientCertificateKeyPassword_.c_str()));
+      }
+
+      // NB: If no "clientKeyFile_" is provided, the key must be
+      // prepended to the certificate file
+      if (!clientCertificateKeyFile_.empty())
+      {
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEYTYPE, "PEM"));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEY, clientCertificateKeyFile_.c_str()));
+      }
+#else
+      LOG(ERROR) << "This version of Orthanc is compiled without OpenSSL support, cannot use HTTPS client authentication";
+      throw OrthancException(ErrorCode_InternalError);
+#endif
+    }
+
     // Reset the parameters from previous calls to Apply()
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, NULL));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->userHeaders_));
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPGET, 0L));
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 0L));
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 0L));
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, NULL));
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL));
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0L));
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, NULL));
 
+    if (redirectionFollowed_)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1L));
+    }
+    else
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 0L));
+    }
+
     // Set timeouts
     if (timeout_ <= 0)
     {
@@ -269,7 +598,12 @@
 
     case HttpMethod_Post:
       CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L));
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->postHeaders_));
+
+      if (pimpl_->userHeaders_ == NULL)
+      {
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->defaultPostHeaders_));
+      }
+
       break;
 
     case HttpMethod_Delete:
@@ -284,7 +618,12 @@
       // CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PUT, 1L));
 
       curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "PUT"); /* !!! */
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->postHeaders_));      
+
+      if (pimpl_->userHeaders_ == NULL)
+      {
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->defaultPostHeaders_));
+      }
+
       break;
 
     default:
@@ -312,6 +651,9 @@
     CURLcode code;
     long status = 0;
 
+    ChunkedBuffer buffer;
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &buffer));
+
     if (boost::starts_with(url_, "https://"))
     {
       code = OrthancHttpClientPerformSSL(pimpl_->curl_, &status);
@@ -321,6 +663,14 @@
       code = GetHttpStatus(curl_easy_perform(pimpl_->curl_), pimpl_->curl_, &status);
     }
 
+    LOG(INFO) << "HTTP status code " << status << " after "
+              << EnumerationToString(method_) << " request on: " << url_;
+
+    if (isVerbose_)
+    {
+      LOG(INFO) << "cURL status code: " << code;
+    }
+
     CheckCode(code);
 
     if (status == 0)
@@ -333,17 +683,31 @@
       lastStatus_ = static_cast<HttpStatus>(status);
     }
 
-    return (status >= 200 && status < 300);
+    bool success = (status >= 200 && status < 300);
+
+    if (success)
+    {
+      buffer.Flatten(answerBody);
+    }
+    else
+    {
+      answerBody.clear();
+      LOG(INFO) << "Error in HTTP request, received HTTP status " << status 
+                << " (" << EnumerationToString(lastStatus_) << ")";
+    }
+
+    return success;
   }
 
 
-  bool HttpClient::Apply(Json::Value& answer)
+  bool HttpClient::ApplyInternal(Json::Value& answerBody,
+                                 HttpClient::HttpHeaders* answerHeaders)
   {
     std::string s;
-    if (Apply(s))
+    if (ApplyInternal(s, answerHeaders))
     {
       Json::Reader reader;
-      return reader.parse(s, answer);
+      return reader.parse(s, answerBody);
     }
     else
     {
@@ -358,75 +722,148 @@
     credentials_ = std::string(username) + ":" + std::string(password);
   }
 
-  
-  const std::string& HttpClient::GetHttpsCACertificates() const
+
+  void HttpClient::ConfigureSsl(bool httpsVerifyPeers,
+                                const std::string& httpsVerifyCertificates)
   {
-    if (caCertificates_.empty())
-    {
-      return globalCACertificates_;
-    }
-    else
-    {
-      return caCertificates_;
-    }
-  }
-
-
-  void HttpClient::GlobalInitialize(bool httpsVerifyPeers,
-                                    const std::string& httpsVerifyCertificates)
-  {
-    globalVerifyPeers_ = httpsVerifyPeers;
-    globalCACertificates_ = httpsVerifyCertificates;
-
-#if ORTHANC_SSL_ENABLED == 1
+#if ORTHANC_ENABLE_SSL == 1
     if (httpsVerifyPeers)
     {
-      if (globalCACertificates_.empty())
+      if (httpsVerifyCertificates.empty())
       {
         LOG(WARNING) << "No certificates are provided to validate peers, "
                      << "set \"HttpsCACertificates\" if you need to do HTTPS requests";
       }
       else
       {
-        LOG(WARNING) << "HTTPS will use the CA certificates from this file: " << globalCACertificates_;
+        LOG(WARNING) << "HTTPS will use the CA certificates from this file: " << httpsVerifyCertificates;
       }
     }
     else
     {
-      LOG(WARNING) << "The verification of the peers in HTTPS requests is disabled!";
+      LOG(WARNING) << "The verification of the peers in HTTPS requests is disabled";
     }
 #endif
 
-    CheckCode(curl_global_init(CURL_GLOBAL_DEFAULT));
+    GlobalParameters::GetInstance().ConfigureSsl(httpsVerifyPeers, httpsVerifyCertificates);
   }
 
   
+  void HttpClient::GlobalInitialize()
+  {
+#if ORTHANC_ENABLE_SSL == 1
+    CheckCode(curl_global_init(CURL_GLOBAL_ALL));
+#else
+    CheckCode(curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_SSL));
+#endif
+  }
+
+
   void HttpClient::GlobalFinalize()
   {
     curl_global_cleanup();
+
+#if ORTHANC_ENABLE_PKCS11 == 1
+    Pkcs11::Finalize();
+#endif
+  }
+  
+
+  void HttpClient::SetDefaultVerbose(bool verbose)
+  {
+    GlobalParameters::GetInstance().SetDefaultVerbose(verbose);
+  }
+
+
+  void HttpClient::SetDefaultProxy(const std::string& proxy)
+  {
+    GlobalParameters::GetInstance().SetDefaultProxy(proxy);
+  }
+
+
+  void HttpClient::SetDefaultTimeout(long timeout)
+  {
+    GlobalParameters::GetInstance().SetDefaultTimeout(timeout);
+  }
+
+
+  void HttpClient::ApplyAndThrowException(std::string& answerBody)
+  {
+    if (!Apply(answerBody))
+    {
+      ThrowException(GetLastStatus());
+    }
   }
 
   
-  void HttpClient::SetDefaultTimeout(long timeout)
+  void HttpClient::ApplyAndThrowException(Json::Value& answerBody)
   {
-    LOG(INFO) << "Setting the default timeout for HTTP client connections: " << timeout << " seconds";
-    globalTimeout_ = timeout;
+    if (!Apply(answerBody))
+    {
+      ThrowException(GetLastStatus());
+    }
   }
 
 
-  void HttpClient::ApplyAndThrowException(std::string& answer)
+  void HttpClient::ApplyAndThrowException(std::string& answerBody,
+                                          HttpHeaders& answerHeaders)
   {
-    if (!Apply(answer))
+    if (!Apply(answerBody, answerHeaders))
     {
       ThrowException(GetLastStatus());
     }
   }
   
-  void HttpClient::ApplyAndThrowException(Json::Value& answer)
+
+  void HttpClient::ApplyAndThrowException(Json::Value& answerBody,
+                                          HttpHeaders& answerHeaders)
   {
-    if (!Apply(answer))
+    if (!Apply(answerBody, answerHeaders))
     {
       ThrowException(GetLastStatus());
     }
   }
+
+
+  void HttpClient::SetClientCertificate(const std::string& certificateFile,
+                                        const std::string& certificateKeyFile,
+                                        const std::string& certificateKeyPassword)
+  {
+    if (certificateFile.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (!SystemToolbox::IsRegularFile(certificateFile))
+    {
+      LOG(ERROR) << "Cannot open certificate file: " << certificateFile;
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+
+    if (!certificateKeyFile.empty() && 
+        !SystemToolbox::IsRegularFile(certificateKeyFile))
+    {
+      LOG(ERROR) << "Cannot open key file: " << certificateKeyFile;
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+
+    clientCertificateFile_ = certificateFile;
+    clientCertificateKeyFile_ = certificateKeyFile;
+    clientCertificateKeyPassword_ = certificateKeyPassword;
+  }
+
+
+  void HttpClient::InitializePkcs11(const std::string& module,
+                                    const std::string& pin,
+                                    bool verbose)
+  {
+#if ORTHANC_ENABLE_PKCS11 == 1
+    LOG(INFO) << "Initializing PKCS#11 using " << module 
+              << (pin.empty() ? " (no PIN provided)" : " (PIN is provided)");
+    GlobalParameters::GetInstance().InitializePkcs11(module, pin, verbose);    
+#else
+    LOG(ERROR) << "This version of Orthanc is compiled without support for PKCS#11";
+    throw OrthancException(ErrorCode_InternalError);
+#endif
+  }
 }
--- a/Core/HttpClient.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpClient.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,16 +34,31 @@
 #pragma once
 
 #include "Enumerations.h"
+#include "WebServiceParameters.h"
 
 #include <string>
 #include <boost/shared_ptr.hpp>
 #include <json/json.h>
 
+#if !defined(ORTHANC_ENABLE_SSL)
+#  error The macro ORTHANC_ENABLE_SSL must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_PKCS11)
+#  error The macro ORTHANC_ENABLE_PKCS11 must be defined
+#endif
+
+
 namespace Orthanc
 {
   class HttpClient
   {
+  public:
+    typedef std::map<std::string, std::string>  HttpHeaders;
+
   private:
+    class GlobalParameters;
+
     struct PImpl;
     boost::shared_ptr<PImpl> pimpl_;
 
@@ -56,15 +72,29 @@
     std::string proxy_;
     bool verifyPeers_;
     std::string caCertificates_;
+    std::string clientCertificateFile_;
+    std::string clientCertificateKeyFile_;
+    std::string clientCertificateKeyPassword_;
+    bool pkcs11Enabled_;
+    bool headersToLowerCase_;
+    bool redirectionFollowed_;
 
     void Setup();
 
-    void operator= (const HttpClient&);  // Forbidden
+    void operator= (const HttpClient&);  // Assignment forbidden
+    HttpClient(const HttpClient& base);  // Copy forbidden
+
+    bool ApplyInternal(std::string& answerBody,
+                       HttpHeaders* answerHeaders);
+
+    bool ApplyInternal(Json::Value& answerBody,
+                       HttpHeaders* answerHeaders);
 
   public:
-    HttpClient(const HttpClient& base);
+    HttpClient();
 
-    HttpClient();
+    HttpClient(const WebServiceParameters& service,
+               const std::string& uri);
 
     ~HttpClient();
 
@@ -125,9 +155,32 @@
       return isVerbose_;
     }
 
-    bool Apply(std::string& answer);
+    void AddHeader(const std::string& key,
+                   const std::string& value);
+
+    void ClearHeaders();
+
+    bool Apply(std::string& answerBody)
+    {
+      return ApplyInternal(answerBody, NULL);
+    }
 
-    bool Apply(Json::Value& answer);
+    bool Apply(Json::Value& answerBody)
+    {
+      return ApplyInternal(answerBody, NULL);
+    }
+
+    bool Apply(std::string& answerBody,
+               HttpHeaders& answerHeaders)
+    {
+      return ApplyInternal(answerBody, &answerHeaders);
+    }
+
+    bool Apply(Json::Value& answerBody,
+               HttpHeaders& answerHeaders)
+    {
+      return ApplyInternal(answerBody, &answerHeaders);
+    }
 
     HttpStatus GetLastStatus() const
     {
@@ -157,17 +210,87 @@
       caCertificates_ = certificates;
     }
 
-    const std::string& GetHttpsCACertificates() const;
+    const std::string& GetHttpsCACertificates() const
+    {
+      return caCertificates_;
+    }
+
+    void SetClientCertificate(const std::string& certificateFile,
+                              const std::string& certificateKeyFile,
+                              const std::string& certificateKeyPassword);
+
+    void SetPkcs11Enabled(bool enabled)
+    {
+      pkcs11Enabled_ = enabled;
+    }
+
+    bool IsPkcs11Enabled() const
+    {
+      return pkcs11Enabled_;
+    }
+
+    const std::string& GetClientCertificateFile() const
+    {
+      return clientCertificateFile_;
+    }
 
-    static void GlobalInitialize(bool httpsVerifyPeers,
-                                 const std::string& httpsCACertificates);
+    const std::string& GetClientCertificateKeyFile() const
+    {
+      return clientCertificateKeyFile_;
+    }
+
+    const std::string& GetClientCertificateKeyPassword() const
+    {
+      return clientCertificateKeyPassword_;
+    }
+
+    void SetConvertHeadersToLowerCase(bool lowerCase)
+    {
+      headersToLowerCase_ = lowerCase;
+    }
+
+    bool IsConvertHeadersToLowerCase() const
+    {
+      return headersToLowerCase_;
+    }
+
+    void SetRedirectionFollowed(bool follow)
+    {
+      redirectionFollowed_ = follow;
+    }
+
+    bool IsRedirectionFollowed() const
+    {
+      return redirectionFollowed_;
+    }
+
+    static void GlobalInitialize();
   
     static void GlobalFinalize();
 
+    static void InitializePkcs11(const std::string& module,
+                                 const std::string& pin,
+                                 bool verbose);
+
+    static void ConfigureSsl(bool httpsVerifyPeers,
+                             const std::string& httpsCACertificates);
+
+    static void SetDefaultVerbose(bool verbose);
+
+    static void SetDefaultProxy(const std::string& proxy);
+
     static void SetDefaultTimeout(long timeout);
 
-    void ApplyAndThrowException(std::string& answer);
+    void ApplyAndThrowException(std::string& answerBody);
+
+    void ApplyAndThrowException(Json::Value& answerBody);
 
-    void ApplyAndThrowException(Json::Value& answer);
+    void ApplyAndThrowException(std::string& answerBody,
+                                HttpHeaders& answerHeaders);
+
+    void ApplyAndThrowException(Json::Value& answerBody,
+                                HttpHeaders& answerHeaders);
+
+    static void ThrowException(HttpStatus status);
   };
 }
--- a/Core/HttpServer/BufferHttpSender.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/BufferHttpSender.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/HttpServer/BufferHttpSender.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/BufferHttpSender.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/HttpServer/EmbeddedResourceHttpHandler.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/EmbeddedResourceHttpHandler.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/HttpServer/EmbeddedResourceHttpHandler.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/EmbeddedResourceHttpHandler.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/HttpServer/FilesystemHttpHandler.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/FilesystemHttpHandler.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,6 +35,7 @@
 #include "FilesystemHttpHandler.h"
 
 #include "../OrthancException.h"
+#include "../SystemToolbox.h"
 #include "FilesystemHttpSender.h"
 
 #include <boost/filesystem.hpp>
@@ -95,8 +97,10 @@
 #endif
 
       std::string h = Toolbox::FlattenUri(uri) + "/" + f;
-      if (fs::is_regular_file(it->status()))
+      if (SystemToolbox::IsRegularFile(it->path().string()))
+      {
         s += "<li><a href=\"" + h + "\">" + f + "</a></li>";
+      }
     }      
 
     s += "    </ul>";
@@ -156,7 +160,7 @@
       p /= uri[i];
     }
 
-    if (fs::exists(p) && fs::is_regular_file(p))
+    if (SystemToolbox::IsRegularFile(p.string()))
     {
       FilesystemHttpSender sender(p);
       output.Answer(sender);   // TODO COMPRESSION
--- a/Core/HttpServer/FilesystemHttpHandler.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/FilesystemHttpHandler.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/HttpServer/FilesystemHttpSender.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/FilesystemHttpSender.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -69,7 +70,7 @@
       throw OrthancException(ErrorCode_CorruptedFile);
     }
 
-    chunkSize_ = file_.gcount();
+    chunkSize_ = static_cast<size_t>(file_.gcount());
 
     return chunkSize_ > 0;
   }
--- a/Core/HttpServer/FilesystemHttpSender.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/FilesystemHttpSender.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -50,12 +51,12 @@
     void Initialize(const boost::filesystem::path& path);
 
   public:
-    FilesystemHttpSender(const std::string& path)
+    explicit FilesystemHttpSender(const std::string& path)
     {
       Initialize(path);
     }
 
-    FilesystemHttpSender(const boost::filesystem::path& path)
+    explicit FilesystemHttpSender(const boost::filesystem::path& path)
     {
       Initialize(path);
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/HttpServer/HttpContentNegociation.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,266 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "HttpContentNegociation.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#include <boost/lexical_cast.hpp>
+
+namespace Orthanc
+{
+  HttpContentNegociation::Handler::Handler(const std::string& type,
+                                           const std::string& subtype,
+                                           IHandler& handler) :
+    type_(type),
+    subtype_(subtype),
+    handler_(handler)
+  {
+  }
+
+
+  bool HttpContentNegociation::Handler::IsMatch(const std::string& type,
+                                                const std::string& subtype) const
+  {
+    if (type == "*" && subtype == "*")
+    {
+      return true;
+    }
+        
+    if (subtype == "*" && type == type_)
+    {
+      return true;
+    }
+
+    return type == type_ && subtype == subtype_;
+  }
+
+
+  struct HttpContentNegociation::Reference : public boost::noncopyable
+  {
+    const Handler&  handler_;
+    uint8_t         level_;
+    float           quality_;
+
+    Reference(const Handler& handler,
+              const std::string& type,
+              const std::string& subtype,
+              float quality) :
+      handler_(handler),
+      quality_(quality)
+    {
+      if (type == "*" && subtype == "*")
+      {
+        level_ = 0;
+      }
+      else if (subtype == "*")
+      {
+        level_ = 1;
+      }
+      else
+      {
+        level_ = 2;
+      }
+    }
+      
+    bool operator< (const Reference& other) const
+    {
+      if (level_ < other.level_)
+      {
+        return true;
+      }
+
+      if (level_ > other.level_)
+      {
+        return false;
+      }
+
+      return quality_ < other.quality_;
+    }
+  };
+
+
+  bool HttpContentNegociation::SplitPair(std::string& first /* out */,
+                                         std::string& second /* out */,
+                                         const std::string& source,
+                                         char separator)
+  {
+    size_t pos = source.find(separator);
+
+    if (pos == std::string::npos)
+    {
+      return false;
+    }
+    else
+    {
+      first = Toolbox::StripSpaces(source.substr(0, pos));
+      second = Toolbox::StripSpaces(source.substr(pos + 1));
+      return true;      
+    }
+  }
+
+
+  float HttpContentNegociation::GetQuality(const Tokens& parameters)
+  {
+    for (size_t i = 1; i < parameters.size(); i++)
+    {
+      std::string key, value;
+      if (SplitPair(key, value, parameters[i], '=') &&
+          key == "q")
+      {
+        float quality;
+        bool ok = false;
+
+        try
+        {
+          quality = boost::lexical_cast<float>(value);
+          ok = (quality >= 0.0f && quality <= 1.0f);
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+        }
+
+        if (ok)
+        {
+          return quality;
+        }
+        else
+        {
+          LOG(ERROR) << "Quality parameter out of range in a HTTP request (must be between 0 and 1): " << value;
+          throw OrthancException(ErrorCode_BadRequest);
+        }
+      }
+    }
+
+    return 1.0f;  // Default quality
+  }
+
+
+  void HttpContentNegociation::SelectBestMatch(std::auto_ptr<Reference>& best,
+                                               const Handler& handler,
+                                               const std::string& type,
+                                               const std::string& subtype,
+                                               float quality)
+  {
+    std::auto_ptr<Reference> match(new Reference(handler, type, subtype, quality));
+
+    if (best.get() == NULL ||
+        *best < *match)
+    {
+      best = match;
+    }
+  }
+
+
+  void HttpContentNegociation::Register(const std::string& mime,
+                                        IHandler& handler)
+  {
+    std::string type, subtype;
+
+    if (SplitPair(type, subtype, mime, '/') &&
+        type != "*" &&
+        subtype != "*")
+    {
+      handlers_.push_back(Handler(type, subtype, handler));
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+    
+  bool HttpContentNegociation::Apply(const HttpHeaders& headers)
+  {
+    HttpHeaders::const_iterator accept = headers.find("accept");
+    if (accept != headers.end())
+    {
+      return Apply(accept->second);
+    }
+    else
+    {
+      return Apply("*/*");
+    }
+  }
+
+
+  bool HttpContentNegociation::Apply(const std::string& accept)
+  {
+    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
+    // https://en.wikipedia.org/wiki/Content_negotiation
+    // http://www.newmediacampaigns.com/blog/browser-rest-http-accept-headers
+
+    Tokens mediaRanges;
+    Toolbox::TokenizeString(mediaRanges, accept, ',');
+
+    std::auto_ptr<Reference> bestMatch;
+
+    for (Tokens::const_iterator it = mediaRanges.begin();
+         it != mediaRanges.end(); ++it)
+    {
+      Tokens parameters;
+      Toolbox::TokenizeString(parameters, *it, ';');
+
+      if (parameters.size() > 0)
+      {
+        float quality = GetQuality(parameters);
+
+        std::string type, subtype;
+        if (SplitPair(type, subtype, parameters[0], '/'))
+        {
+          for (Handlers::const_iterator it2 = handlers_.begin();
+               it2 != handlers_.end(); ++it2)
+          {
+            if (it2->IsMatch(type, subtype))
+            {
+              SelectBestMatch(bestMatch, *it2, type, subtype, quality);
+            }
+          }
+        }
+      }
+    }
+
+    if (bestMatch.get() == NULL)  // No match was found
+    {
+      return false;
+    }
+    else
+    {
+      bestMatch->handler_.Call();
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/HttpServer/HttpContentNegociation.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include <memory>
+#include <boost/noncopyable.hpp>
+#include <map>
+#include <list>
+#include <string>
+#include <vector>
+#include <stdint.h>
+
+
+namespace Orthanc
+{
+  class HttpContentNegociation : public boost::noncopyable
+  {
+  public:
+    typedef std::map<std::string, std::string>  HttpHeaders;
+
+    class IHandler : public boost::noncopyable
+    {
+    public:
+      virtual ~IHandler()
+      {
+      }
+
+      virtual void Handle(const std::string& type,
+                          const std::string& subtype) = 0;
+    };
+
+  private:
+    struct Handler
+    {
+      std::string  type_;
+      std::string  subtype_;
+      IHandler&    handler_;
+
+      Handler(const std::string& type,
+              const std::string& subtype,
+              IHandler& handler);
+
+      bool IsMatch(const std::string& type,
+                   const std::string& subtype) const;
+
+      void Call() const
+      {
+        handler_.Handle(type_, subtype_);
+      }
+   };
+
+
+    struct Reference;
+
+    typedef std::vector<std::string>  Tokens;
+    typedef std::list<Handler>   Handlers;
+
+    Handlers  handlers_;
+
+
+    static bool SplitPair(std::string& first /* out */,
+                          std::string& second /* out */,
+                          const std::string& source,
+                          char separator);
+
+    static float GetQuality(const Tokens& parameters);
+
+    static void SelectBestMatch(std::auto_ptr<Reference>& best,
+                                const Handler& handler,
+                                const std::string& type,
+                                const std::string& subtype,
+                                float quality);
+
+  public:
+    void Register(const std::string& mime,
+                  IHandler& handler);
+    
+    bool Apply(const HttpHeaders& headers);
+
+    bool Apply(const std::string& accept);
+  };
+}
--- a/Core/HttpServer/HttpFileSender.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/HttpFileSender.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/HttpServer/HttpFileSender.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/HttpFileSender.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/HttpServer/HttpOutput.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/HttpOutput.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -291,8 +292,7 @@
 			      const char* message,
 			      size_t messageSize)
   {
-    if (status == HttpStatus_200_Ok ||
-        status == HttpStatus_301_MovedPermanently ||
+    if (status == HttpStatus_301_MovedPermanently ||
         status == HttpStatus_401_Unauthorized ||
         status == HttpStatus_405_MethodNotAllowed)
     {
@@ -300,7 +300,6 @@
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
     
-    stateMachine_.ClearHeaders();
     stateMachine_.SetHttpStatus(status);
     stateMachine_.SendBody(message, messageSize);
   }
@@ -433,28 +432,103 @@
     {
       if (!Toolbox::StartsWith(*it, "Set-Cookie: "))
       {
-        LOG(ERROR) << "The only headers that can be set in multipart answers are Set-Cookie (here: " << *it << " is set)";
+        LOG(ERROR) << "The only headers that can be set in multipart answers "
+                   << "are Set-Cookie (here: " << *it << " is set)";
         throw OrthancException(ErrorCode_BadSequenceOfCalls);
       }
 
       header += *it;
     }
 
+    /**
+     * Fix for issue 54 ("Decide what to do wrt. quoting of multipart
+     * answers"). The "type" parameter in the "Content-Type" HTTP
+     * header must be quoted if it contains a forward slash "/". This
+     * is necessary for DICOMweb compatibility with OsiriX, but breaks
+     * compatibility with old releases of the client in the Orthanc
+     * DICOMweb plugin <= 0.3 (releases >= 0.4 work fine).
+     *
+     * Full history is available at the following locations:
+     * - In changeset 2248:69b0f4e8a49b:
+     *   # hg history -v -r 2248
+     * - https://bitbucket.org/sjodogne/orthanc/issues/54/
+     * - https://groups.google.com/d/msg/orthanc-users/65zhIM5xbKI/TU5Q1_LhAwAJ
+     **/
+    std::string tmp;
+    if (contentType.find('/') == std::string::npos)
+    {
+      // No forward slash in the content type
+      tmp = contentType;
+    }
+    else
+    {
+      // Quote the content type because of the forward slash
+      tmp = "\"" + contentType + "\"";
+    }
+
     multipartBoundary_ = Toolbox::GenerateUuid();
     multipartContentType_ = contentType;
-    header += "Content-Type: multipart/related; type=multipart/" + subType + "; boundary=" + multipartBoundary_ + "\r\n\r\n";
+    header += ("Content-Type: multipart/" + subType + "; type=" +
+               tmp + "; boundary=" + multipartBoundary_ + "\r\n\r\n");
 
     stream_.Send(true, header.c_str(), header.size());
     state_ = State_WritingMultipart;
   }
 
 
-  void HttpOutput::StateMachine::SendMultipartItem(const void* item, size_t length)
+  void HttpOutput::StateMachine::SendMultipartItem(const void* item, 
+                                                   size_t length,
+                                                   const std::map<std::string, std::string>& headers)
   {
-    std::string header = "--" + multipartBoundary_ + "\n";
-    header += "Content-Type: " + multipartContentType_ + "\n";
-    header += "Content-Length: " + boost::lexical_cast<std::string>(length) + "\n";
-    header += "MIME-Version: 1.0\n\n";
+    if (state_ != State_WritingMultipart)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    std::string header = "--" + multipartBoundary_ + "\r\n";
+
+    bool hasContentType = false;
+    bool hasContentLength = false;
+    bool hasMimeVersion = false;
+
+    for (std::map<std::string, std::string>::const_iterator
+           it = headers.begin(); it != headers.end(); ++it)
+    {
+      header += it->first + ": " + it->second + "\r\n";
+
+      std::string tmp;
+      Toolbox::ToLowerCase(tmp, it->first);
+
+      if (tmp == "content-type")
+      {
+        hasContentType = true;
+      }
+
+      if (tmp == "content-length")
+      {
+        hasContentLength = true;
+      }
+
+      if (tmp == "mime-version")
+      {
+        hasMimeVersion = true;
+      }
+    }
+
+    if (!hasContentType)
+    {
+      header += "Content-Type: " + multipartContentType_ + "\r\n";
+    }
+
+    if (!hasContentLength)
+    {
+      header += "Content-Length: " + boost::lexical_cast<std::string>(length) + "\r\n";
+    }
+
+    if (!hasMimeVersion)
+    {
+      header += "MIME-Version: 1.0\r\n\r\n";
+    }
 
     stream_.Send(false, header.c_str(), header.size());
 
@@ -463,7 +537,7 @@
       stream_.Send(false, item, length);
     }
 
-    stream_.Send(false, "\n", 1);
+    stream_.Send(false, "\r\n", 2);    
   }
 
 
@@ -478,7 +552,7 @@
     // closed the connection. Such an error is ignored.
     try
     {
-      std::string header = "--" + multipartBoundary_ + "--\n";
+      std::string header = "--" + multipartBoundary_ + "--\r\n";
       stream_.Send(false, header.c_str(), header.size());
     }
     catch (OrthancException&)
@@ -489,19 +563,6 @@
   }
 
 
-  void HttpOutput::SendMultipartItem(const std::string& item)
-  {
-    if (item.size() > 0)
-    {
-      stateMachine_.SendMultipartItem(item.c_str(), item.size());
-    }
-    else
-    {
-      stateMachine_.SendMultipartItem(NULL, 0);
-    }
-  }
-
-
   void HttpOutput::Answer(IHttpStreamAnswer& stream)
   {
     HttpCompression compression = stream.SetupHttpCompression(isGzipAllowed_, isDeflateAllowed_);
--- a/Core/HttpServer/HttpOutput.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/HttpOutput.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,13 +33,14 @@
 
 #pragma once
 
+#include "../Enumerations.h"
+#include "IHttpOutputStream.h"
+#include "IHttpStreamAnswer.h"
+
 #include <list>
 #include <string>
 #include <stdint.h>
-#include "../Enumerations.h"
-#include "IHttpOutputStream.h"
-#include "IHttpStreamAnswer.h"
-#include "../Uuid.h"
+#include <map>
 
 namespace Orthanc
 {
@@ -99,7 +101,9 @@
       void StartMultipart(const std::string& subType,
                           const std::string& contentType);
 
-      void SendMultipartItem(const void* item, size_t length);
+      void SendMultipartItem(const void* item, 
+                             size_t length,
+                             const std::map<std::string, std::string>& headers);
 
       void CloseMultipart();
 
@@ -202,11 +206,11 @@
       stateMachine_.StartMultipart(subType, contentType);
     }
 
-    void SendMultipartItem(const std::string& item);
-
-    void SendMultipartItem(const void* item, size_t size)
+    void SendMultipartItem(const void* item, 
+                           size_t size,
+                           const std::map<std::string, std::string>& headers)
     {
-      stateMachine_.SendMultipartItem(item, size);
+      stateMachine_.SendMultipartItem(item, size, headers);
     }
 
     void CloseMultipart()
--- a/Core/HttpServer/HttpStreamTranscoder.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/HttpStreamTranscoder.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/HttpServer/HttpStreamTranscoder.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/HttpStreamTranscoder.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -61,6 +62,7 @@
       sourceCompression_(compression),
       bytesToSkip_(0),
       skipped_(0),
+      currentChunkOffset_(0),
       ready_(false)
     {
     }
--- a/Core/HttpServer/HttpToolbox.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/HttpToolbox.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -41,7 +42,7 @@
 #include "StringHttpOutput.h"
 
 
-static const char* LOCALHOST = "localhost";
+static const char* LOCALHOST = "127.0.0.1";
 
 
 
@@ -201,10 +202,9 @@
   bool HttpToolbox::SimpleGet(std::string& result,
                               IHttpHandler& handler,
                               RequestOrigin origin,
-                              const std::string& uri)
+                              const std::string& uri,
+                              const IHttpHandler::Arguments& httpHeaders)
   {
-    IHttpHandler::Arguments headers;  // No HTTP header
-
     UriComponents curi;
     IHttpHandler::GetArguments getArguments;
     ParseGetQuery(curi, getArguments, uri.c_str());
@@ -213,7 +213,7 @@
     HttpOutput http(stream, false /* no keep alive */);
 
     if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Get, curi, 
-                       headers, getArguments, NULL /* no body for GET */, 0))
+                       httpHeaders, getArguments, NULL /* no body for GET */, 0))
     {
       stream.GetOutput(result);
       return true;
@@ -225,6 +225,16 @@
   }
 
 
+  bool HttpToolbox::SimpleGet(std::string& result,
+                              IHttpHandler& handler,
+                              RequestOrigin origin,
+                              const std::string& uri)
+  {
+    IHttpHandler::Arguments headers;  // No HTTP header
+    return SimpleGet(result, handler, origin, uri, headers);
+  }
+
+
   static bool SimplePostOrPut(std::string& result,
                               IHttpHandler& handler,
                               RequestOrigin origin,
--- a/Core/HttpServer/HttpToolbox.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/HttpToolbox.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -65,6 +66,12 @@
                           RequestOrigin origin,
                           const std::string& uri);
 
+    static bool SimpleGet(std::string& result,
+                          IHttpHandler& handler,
+                          RequestOrigin origin,
+                          const std::string& uri,
+                          const IHttpHandler::Arguments& httpHeaders);
+
     static bool SimplePost(std::string& result,
                            IHttpHandler& handler,
                            RequestOrigin origin,
--- a/Core/HttpServer/IHttpHandler.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/IHttpHandler.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,7 +33,7 @@
 
 #pragma once
 
-#include "../Enumerations.h"
+#include "../Toolbox.h"
 #include "HttpOutput.h"
 
 #include <map>
--- a/Core/HttpServer/IHttpOutputStream.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/IHttpOutputStream.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/HttpServer/IHttpStreamAnswer.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/IHttpStreamAnswer.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/HttpServer/IIncomingHttpRequestFilter.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,54 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IHttpHandler.h"
+
+namespace Orthanc
+{
+  class IIncomingHttpRequestFilter : public boost::noncopyable
+  {
+  public:
+    virtual ~IIncomingHttpRequestFilter()
+    {
+    }
+
+    virtual bool IsAllowed(HttpMethod method,
+                           const char* uri,
+                           const char* ip,
+                           const char* username,
+                           const IHttpHandler::Arguments& httpHeaders,
+                           const IHttpHandler::GetArguments& getArguments) = 0;
+  };
+}
--- a/Core/HttpServer/MongooseServer.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/MongooseServer.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -38,7 +39,17 @@
 #include "../Logging.h"
 #include "../ChunkedBuffer.h"
 #include "HttpToolbox.h"
-#include "mongoose.h"
+
+#if ORTHANC_ENABLE_MONGOOSE == 1
+#  include "mongoose.h"
+
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+#  include "civetweb.h"
+#  define MONGOOSE_USE_CALLBACKS 1
+
+#else
+#  error "Either Mongoose or Civetweb must be enabled to compile this file"
+#endif
 
 #include <algorithm>
 #include <string.h>
@@ -49,14 +60,16 @@
 #include <stdio.h>
 #include <boost/thread.hpp>
 
-#if ORTHANC_SSL_ENABLED == 1
+#if !defined(ORTHANC_ENABLE_SSL)
+#  error The macro ORTHANC_ENABLE_SSL must be defined
+#endif
+
+#if ORTHANC_ENABLE_SSL == 1
 #include <openssl/opensslv.h>
 #endif
 
 #define ORTHANC_REALM "Orthanc Secure Area"
 
-static const long LOCALHOST = (127ll << 24) + 1ll;
-
 
 namespace Orthanc
 {
@@ -279,7 +292,7 @@
     {
       length = boost::lexical_cast<int>(cs->second);
     }
-    catch (boost::bad_lexical_cast)
+    catch (boost::bad_lexical_cast&)
     {
       return PostDataStatus_NoLength;
     }
@@ -351,7 +364,7 @@
       {
         fileSize = boost::lexical_cast<size_t>(fileSizeStr->second);
       }
-      catch (boost::bad_lexical_cast)
+      catch (boost::bad_lexical_cast&)
       {
         return PostDataStatus_Failure;
       }
@@ -404,7 +417,7 @@
         last = it;
       }
     }
-    catch (std::length_error)
+    catch (std::length_error&)
     {
       return PostDataStatus_Failure;
     }
@@ -575,19 +588,30 @@
   }
 
 
-  static void InternalCallback(struct mg_connection *connection,
+  static void InternalCallback(HttpOutput& output /* out */,
+                               HttpMethod& method /* out */,
+                               MongooseServer& server,
+                               struct mg_connection *connection,
                                const struct mg_request_info *request)
   {
-    MongooseServer* that = reinterpret_cast<MongooseServer*>(request->user_data);
-
-    MongooseOutputStream stream(connection);
-    HttpOutput output(stream, that->IsKeepAliveEnabled());
+    bool localhost;
 
+#if ORTHANC_ENABLE_MONGOOSE == 1
+    static const long LOCALHOST = (127ll << 24) + 1ll;
+    localhost = (request->remote_ip == LOCALHOST);
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+    // The "remote_ip" field of "struct mg_request_info" is tagged as
+    // deprecated in Civetweb, using "remote_addr" instead.
+    localhost = (std::string(request->remote_addr) == "127.0.0.1");
+#else
+#error
+#endif
+    
     // Check remote calls
-    if (!that->IsRemoteAccessAllowed() &&
-        request->remote_ip != LOCALHOST)
+    if (!server.IsRemoteAccessAllowed() &&
+        !localhost)
     {
-      output.SendUnauthorized(ORTHANC_REALM);
+      output.SendUnauthorized(server.GetRealm());
       return;
     }
 
@@ -597,11 +621,14 @@
     for (int i = 0; i < request->num_headers; i++)
     {
       std::string name = request->http_headers[i].name;
+      std::string value = request->http_headers[i].value;
+
       std::transform(name.begin(), name.end(), name.begin(), ::tolower);
-      headers.insert(std::make_pair(name, request->http_headers[i].value));
+      headers.insert(std::make_pair(name, value));
+      VLOG(1) << "HTTP header: [" << name << "]: [" << value << "]";
     }
 
-    if (that->IsHttpCompressionEnabled())
+    if (server.IsHttpCompressionEnabled())
     {
       ConfigureHttpCompression(output, headers);
     }
@@ -616,7 +643,7 @@
 
 
     // Compute the HTTP method, taking method faking into consideration
-    HttpMethod method = HttpMethod_Get;
+    method = HttpMethod_Get;
     if (!ExtractMethod(method, request, headers, argumentsGET))
     {
       output.SendStatus(HttpStatus_400_BadRequest);
@@ -625,13 +652,15 @@
 
 
     // Authenticate this connection
-    if (that->IsAuthenticationEnabled() && !IsAccessGranted(*that, headers))
+    if (server.IsAuthenticationEnabled() && 
+        !IsAccessGranted(server, headers))
     {
-      output.SendUnauthorized(ORTHANC_REALM);
+      output.SendUnauthorized(server.GetRealm());
       return;
     }
 
-
+    
+#if ORTHANC_ENABLE_MONGOOSE == 1
     // Apply the filter, if it is installed
     char remoteIp[24];
     sprintf(remoteIp, "%d.%d.%d.%d", 
@@ -639,15 +668,22 @@
             reinterpret_cast<const uint8_t*>(&request->remote_ip) [2], 
             reinterpret_cast<const uint8_t*>(&request->remote_ip) [1], 
             reinterpret_cast<const uint8_t*>(&request->remote_ip) [0]);
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+    const char* remoteIp = request->remote_addr;
+#else
+#error
+#endif
 
     std::string username = GetAuthenticatedUsername(headers);
 
-    const IIncomingHttpRequestFilter *filter = that->GetIncomingHttpRequestFilter();
+    IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter();
     if (filter != NULL)
     {
-      if (!filter->IsAllowed(method, request->uri, remoteIp, username.c_str()))
+      if (!filter->IsAllowed(method, request->uri, remoteIp,
+                             username.c_str(), headers, argumentsGET))
       {
-        output.SendUnauthorized(ORTHANC_REALM);
+        //output.SendUnauthorized(server.GetRealm());
+        output.SendStatus(HttpStatus_403_Forbidden);
         return;
       }
     }
@@ -675,7 +711,7 @@
         if (contentType.size() >= multipartLength &&
             !memcmp(contentType.c_str(), multipart, multipartLength))
         {
-          status = ParseMultipartPost(body, connection, headers, contentType, that->GetChunkStore());
+          status = ParseMultipartPost(body, connection, headers, contentType, server.GetChunkStore());
         }
         else
         {
@@ -709,7 +745,7 @@
     {
       Toolbox::SplitUriComponents(uri, request->uri);
     }
-    catch (OrthancException)
+    catch (OrthancException&)
     {
       output.SendStatus(HttpStatus_400_BadRequest);
       return;
@@ -718,56 +754,111 @@
 
     LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri);
 
+    bool found = false;
 
+    if (server.HasHandler())
+    {
+      found = server.GetHandler().Handle(output, RequestOrigin_RestApi, remoteIp, username.c_str(), 
+                                         method, uri, headers, argumentsGET, body.c_str(), body.size());
+    }
+
+    if (!found)
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+
+
+  static void ProtectedCallback(struct mg_connection *connection,
+                                const struct mg_request_info *request)
+  {
     try
     {
-      bool found = false;
+      void* that = NULL;
+
+#if ORTHANC_ENABLE_MONGOOSE == 1
+      that = request->user_data;
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+      // https://github.com/civetweb/civetweb/issues/409
+      that = mg_get_user_data(mg_get_context(connection));
+#else
+#error
+#endif                              
+      
+      MongooseServer* server = reinterpret_cast<MongooseServer*>(that);
+
+      if (server == NULL)
+      {
+        MongooseOutputStream stream(connection);
+        HttpOutput output(stream, false /* assume no keep-alive */);
+        output.SendStatus(HttpStatus_500_InternalServerError);
+        return;
+      }
+
+      MongooseOutputStream stream(connection);
+      HttpOutput output(stream, server->IsKeepAliveEnabled());
+      HttpMethod method = HttpMethod_Get;
 
       try
       {
-        if (that->HasHandler())
+        try
+        {
+          InternalCallback(output, method, *server, connection, request);
+        }
+        catch (OrthancException&)
+        {
+          throw;  // Pass the exception to the main handler below
+        }
+        // Now convert native exceptions as OrthancException
+        catch (boost::bad_lexical_cast&)
         {
-          found = that->GetHandler().Handle(output, RequestOrigin_Http, remoteIp, username.c_str(), 
-                                            method, uri, headers, argumentsGET, body.c_str(), body.size());
+          LOG(ERROR) << "Syntax error in some user-supplied data";
+          throw OrthancException(ErrorCode_BadParameterType);
+        }
+        catch (std::runtime_error&)
+        {
+          // Presumably an error while parsing the JSON body
+          throw OrthancException(ErrorCode_BadRequest);
+        }
+        catch (std::bad_alloc&)
+        {
+          LOG(ERROR) << "The server hosting Orthanc is running out of memory";
+          throw OrthancException(ErrorCode_NotEnoughMemory);
+        }
+        catch (...)
+        {
+          LOG(ERROR) << "An unhandled exception was generated inside the HTTP server";
+          throw OrthancException(ErrorCode_InternalError);
         }
       }
-      catch (boost::bad_lexical_cast&)
-      {
-        throw OrthancException(ErrorCode_BadParameterType);
-      }
-      catch (std::runtime_error&)
+      catch (OrthancException& e)
       {
-        // Presumably an error while parsing the JSON body
-        throw OrthancException(ErrorCode_BadRequest);
-      }
+        assert(server != NULL);
 
-      if (!found)
-      {
-        throw OrthancException(ErrorCode_UnknownResource);
+        // Using this candidate handler results in an exception
+        try
+        {
+          if (server->GetExceptionFormatter() == NULL)
+          {
+            LOG(ERROR) << "Exception in the HTTP handler: " << e.What();
+            output.SendStatus(e.GetHttpStatus());
+          }
+          else
+          {
+            server->GetExceptionFormatter()->Format(output, e, method, request->uri);
+          }
+        }
+        catch (OrthancException&)
+        {
+          // An exception here reflects the fact that the status code
+          // was already set by the HTTP handler.
+        }
       }
     }
-    catch (OrthancException& e)
+    catch (...)
     {
-      // Using this candidate handler results in an exception
-      try
-      {
-        if (that->GetExceptionFormatter() == NULL)
-        {
-          LOG(ERROR) << "Exception in the HTTP handler: " << e.What();
-          output.SendStatus(e.GetHttpStatus());
-        }
-        else
-        {
-          that->GetExceptionFormatter()->Format(output, e, method, request->uri);
-        }
-      }
-      catch (OrthancException&)
-      {
-        // An exception here reflects the fact that the status code
-        // was already set by the HTTP handler.
-      }
-
-      return;
+      // We should never arrive at this point, where it is even impossible to send an answer
+      LOG(ERROR) << "Catastrophic error inside the HTTP server, giving up";
     }
   }
 
@@ -779,7 +870,7 @@
   {
     if (event == MG_NEW_REQUEST) 
     {
-      InternalCallback(connection, request);
+      ProtectedCallback(connection, request);
 
       // Mark as processed
       return (void*) "";
@@ -793,9 +884,9 @@
 #elif MONGOOSE_USE_CALLBACKS == 1
   static int Callback(struct mg_connection *connection)
   {
-    struct mg_request_info *request = mg_get_request_info(connection);
+    const struct mg_request_info *request = mg_get_request_info(connection);
 
-    InternalCallback(connection, request);
+    ProtectedCallback(connection, request);
 
     return 1;  // Do not let Mongoose handle the request by itself
   }
@@ -826,8 +917,9 @@
     keepAlive_ = false;
     httpCompression_ = true;
     exceptionFormatter_ = NULL;
+    realm_ = ORTHANC_REALM;
 
-#if ORTHANC_SSL_ENABLED == 1
+#if ORTHANC_ENABLE_SSL == 1
     // Check for the Heartbleed exploit
     // https://en.wikipedia.org/wiki/OpenSSL#Heartbleed_bug
     if (OPENSSL_VERSION_NUMBER <  0x1000107fL  /* openssl-1.0.1g */ &&
@@ -853,6 +945,14 @@
 
   void MongooseServer::Start()
   {
+#if ORTHANC_ENABLE_MONGOOSE == 1
+    LOG(INFO) << "Starting embedded Web server using Mongoose";
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+    LOG(INFO) << "Starting embedded Web server using Civetweb";
+#else
+#error
+#endif  
+
     if (!IsRunning())
     {
       std::string port = boost::lexical_cast<std::string>(port_);
@@ -870,6 +970,11 @@
         // https://groups.google.com/d/msg/orthanc-users/CKueKX0pJ9E/_UCbl8T-VjIJ
         "enable_keep_alive", (keepAlive_ ? "yes" : "no"),
 
+#if ORTHANC_ENABLE_CIVETWEB == 1
+        // https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md#enable_keep_alive-no
+        "keep_alive_timeout_ms", (keepAlive_ ? "500" : "0"),
+#endif
+        
         // Set the SSL certificate, if any. This must be the last option.
         ssl_ ? "ssl_certificate" : NULL,
         certificate_.c_str(),
@@ -893,6 +998,13 @@
       {
         throw OrthancException(ErrorCode_HttpPortInUse);
       }
+
+      LOG(WARNING) << "HTTP server listening on port: " << GetPortNumber()
+                   << " (HTTPS encryption is "
+                   << (IsSslEnabled() ? "enabled" : "disabled")
+                   << ", remote access is "
+                   << (IsRemoteAccessAllowed() ? "" : "not ")
+                   << "allowed)";
     }
   }
 
@@ -928,7 +1040,7 @@
   {
     Stop();
 
-#if ORTHANC_SSL_ENABLED == 0
+#if ORTHANC_ENABLE_SSL == 0
     if (enabled)
     {
       throw OrthancException(ErrorCode_SslDisabled);
@@ -947,7 +1059,7 @@
   {
     Stop();
     keepAlive_ = enabled;
-    LOG(WARNING) << "HTTP keep alive is " << (enabled ? "enabled" : "disabled");
+    LOG(INFO) << "HTTP keep alive is " << (enabled ? "enabled" : "disabled");
   }
 
 
--- a/Core/HttpServer/MongooseServer.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/MongooseServer.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,7 +33,21 @@
 
 #pragma once
 
-#include "IHttpHandler.h"
+#if !defined(ORTHANC_ENABLE_MONGOOSE)
+#  error Macro ORTHANC_ENABLE_MONGOOSE must be defined to include this file
+#endif
+
+#if !defined(ORTHANC_ENABLE_CIVETWEB)
+#  error Macro ORTHANC_ENABLE_CIVETWEB must be defined to include this file
+#endif
+
+#if (ORTHANC_ENABLE_MONGOOSE == 0 && \
+     ORTHANC_ENABLE_CIVETWEB == 0)
+#  error Either ORTHANC_ENABLE_MONGOOSE or ORTHANC_ENABLE_CIVETWEB must be set to 1
+#endif
+
+
+#include "IIncomingHttpRequestFilter.h"
 
 #include "../OrthancException.h"
 
@@ -46,21 +61,7 @@
 {
   class ChunkStore;
 
-  class IIncomingHttpRequestFilter
-  {
-  public:
-    virtual ~IIncomingHttpRequestFilter()
-    {
-    }
-
-    virtual bool IsAllowed(HttpMethod method,
-                           const char* uri,
-                           const char* ip,
-                           const char* username) const = 0;
-  };
-
-
-  class IHttpExceptionFormatter
+  class IHttpExceptionFormatter : public boost::noncopyable
   {
   public:
     virtual ~IHttpExceptionFormatter()
@@ -95,6 +96,7 @@
     bool keepAlive_;
     bool httpCompression_;
     IHttpExceptionFormatter* exceptionFormatter_;
+    std::string realm_;
   
     bool IsRunning() const;
 
@@ -161,7 +163,7 @@
 
     void SetHttpCompressionEnabled(bool enabled);
 
-    const IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const
+    IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const
     {
       return filter_;
     }
@@ -187,5 +189,15 @@
     {
       return exceptionFormatter_;
     }
+
+    const std::string& GetRealm() const
+    {
+      return realm_;
+    }
+
+    void SetRealm(const std::string& realm)
+    {
+      realm_ = realm;
+    }
   };
 }
--- a/Core/HttpServer/StringHttpOutput.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/StringHttpOutput.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -39,9 +40,18 @@
 {
   void StringHttpOutput::OnHttpStatusReceived(HttpStatus status)
   {
-    if (status != HttpStatus_200_Ok)
+    switch (status)
     {
-      throw OrthancException(ErrorCode_BadRequest);
+      case HttpStatus_200_Ok:
+        found_ = true;
+        break;
+
+      case HttpStatus_404_NotFound:
+        found_ = false;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_BadRequest);
     }
   }
 
@@ -52,4 +62,16 @@
       buffer_.AddChunk(reinterpret_cast<const char*>(buffer), length);
     }
   }
+
+  void StringHttpOutput::GetOutput(std::string& output)
+  {
+    if (found_)
+    {
+      buffer_.Flatten(output);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
 }
--- a/Core/HttpServer/StringHttpOutput.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/HttpServer/StringHttpOutput.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -41,16 +42,18 @@
   class StringHttpOutput : public IHttpOutputStream
   {
   private:
+    bool          found_;
     ChunkedBuffer buffer_;
 
   public:
+    StringHttpOutput() : found_(false)
+    {
+    }
+
     virtual void OnHttpStatusReceived(HttpStatus status);
 
     virtual void Send(bool isHeader, const void* buffer, size_t length);
 
-    void GetOutput(std::string& output)
-    {
-      buffer_.Flatten(output);
-    }
+    void GetOutput(std::string& output);
   };
 }
--- a/Core/ICommand.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IDynamicObject.h"
-
-namespace Orthanc
-{
-  /**
-   * This class is the base class for the "Command" design pattern.
-   * http://en.wikipedia.org/wiki/Command_pattern
-   **/
-  class ICommand : public IDynamicObject
-  {
-  public:
-    virtual bool Execute() = 0;
-  };
-}
--- a/Core/IDynamicObject.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/IDynamicObject.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -49,4 +50,27 @@
     {
     }
   };
+
+  /**
+   * This class is a simple implementation of a IDynamicObject that stores a single typed value
+   */
+  template <typename T>
+  class SingleValueObject : public Orthanc::IDynamicObject
+  {
+  private:
+    T                  value_;
+  public:
+    SingleValueObject(const T& value) :
+      value_(value)
+    {
+    }
+    virtual ~SingleValueObject()
+    {
+    }
+
+    const T& GetValue() const
+    {
+        return value_;
+    }
+  };
 }
--- a/Core/Images/Font.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/Font.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,6 +34,14 @@
 #include "../PrecompiledHeaders.h"
 #include "Font.h"
 
+#if !defined(ORTHANC_ENABLE_LOCALE)
+#  error ORTHANC_ENABLE_LOCALE must be defined to use this file
+#endif
+
+#if ORTHANC_SANDBOXED == 0
+#  include "../SystemToolbox.h"
+#endif
+
 #include "../Toolbox.h"
 #include "../OrthancException.h"
 
@@ -132,12 +141,14 @@
   }
 
 
+#if ORTHANC_SANDBOXED == 0
   void Font::LoadFromFile(const std::string& path)
   {
     std::string font;
-    Toolbox::ReadFile(font, path);
+    SystemToolbox::ReadFile(font, path);
     LoadFromMemory(font);
   }
+#endif
 
 
   static unsigned int MyMin(unsigned int a, 
@@ -208,6 +219,7 @@
         }
 
         case PixelFormat_RGBA32:
+        case PixelFormat_BGRA32:
         {
           assert(bpp == 4);
 
@@ -245,14 +257,21 @@
   {
     if (target.GetFormat() != PixelFormat_Grayscale8 &&
         target.GetFormat() != PixelFormat_RGB24 &&
-        target.GetFormat() != PixelFormat_RGBA32)
+        target.GetFormat() != PixelFormat_RGBA32 &&
+        target.GetFormat() != PixelFormat_BGRA32)
     {
       throw OrthancException(ErrorCode_NotImplemented);
     }
 
     int a = x;
 
+#if ORTHANC_ENABLE_LOCALE == 1
     std::string s = Toolbox::ConvertFromUtf8(utf8, Encoding_Latin1);
+#else
+    // If the locale support is disabled, simply drop non-ASCII
+    // characters from the source UTF-8 string
+    std::string s = Toolbox::ConvertToAscii(utf8);
+#endif
 
     for (size_t i = 0; i < s.size(); i++)
     {
@@ -294,7 +313,25 @@
                   uint8_t g,
                   uint8_t b) const
   {
-    uint8_t color[4] = { r, g, b, 255 };
+    uint8_t color[4];
+
+    switch (target.GetFormat())
+    {
+      case PixelFormat_BGRA32:
+        color[0] = b;
+        color[1] = g;
+        color[2] = r;
+        color[3] = 255;
+        break;
+
+      default:
+        color[0] = r;
+        color[1] = g;
+        color[2] = b;
+        color[3] = 255;
+        break;
+    }
+    
     DrawInternal(target, utf8, x, y, color);
   }
 
--- a/Core/Images/Font.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/Font.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -83,7 +84,9 @@
 
     void LoadFromMemory(const std::string& font);
 
+#if ORTHANC_SANDBOXED == 0
     void LoadFromFile(const std::string& path);
+#endif
 
     const std::string& GetName() const
     {
--- a/Core/Images/FontRegistry.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/FontRegistry.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -56,20 +57,24 @@
   }
 
 
+#if ORTHANC_SANDBOXED == 0
   void FontRegistry::AddFromFile(const std::string& path)
   {
     std::auto_ptr<Font> f(new Font);
     f->LoadFromFile(path);
     fonts_.push_back(f.release());
   }
+#endif
 
 
+#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1
   void FontRegistry::AddFromResource(EmbeddedResources::FileResourceId resource)
   {
     std::string content;
     EmbeddedResources::GetFileResource(content, resource);
     AddFromMemory(content);
   }
+#endif
 
 
   const Font& FontRegistry::GetFont(size_t i) const
--- a/Core/Images/FontRegistry.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/FontRegistry.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,7 +35,13 @@
 
 #include "Font.h"
 
-#include <EmbeddedResources.h>   // Autogenerated file
+#if !defined(ORTHANC_HAS_EMBEDDED_RESOURCES)
+#  error Macro ORTHANC_HAS_EMBEDDED_RESOURCES must be defined
+#endif
+
+#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1
+#  include <EmbeddedResources.h>   // Autogenerated file
+#endif
 
 namespace Orthanc
 {
@@ -50,9 +57,13 @@
 
     void AddFromMemory(const std::string& font);
 
+#if ORTHANC_SANDBOXED == 0
     void AddFromFile(const std::string& path);
+#endif
 
+#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1
     void AddFromResource(EmbeddedResources::FileResourceId resource);
+#endif
 
     size_t GetSize() const
     {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/IImageWriter.cpp	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "IImageWriter.h"
+
+#if ORTHANC_SANDBOXED == 0
+#  include "../SystemToolbox.h"
+#endif
+
+namespace Orthanc
+{
+#if ORTHANC_SANDBOXED == 0
+  void IImageWriter::WriteToFileInternal(const std::string& path,
+                                         unsigned int width,
+                                         unsigned int height,
+                                         unsigned int pitch,
+                                         PixelFormat format,
+                                         const void* buffer)
+  {
+    std::string compressed;
+    WriteToMemoryInternal(compressed, width, height, pitch, format, buffer);
+    SystemToolbox::WriteFile(compressed, path);
+  }
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/IImageWriter.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ImageAccessor.h"
+
+#include <boost/noncopyable.hpp>
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+namespace Orthanc
+{
+  class IImageWriter : public boost::noncopyable
+  {
+  protected:
+    virtual void WriteToMemoryInternal(std::string& compressed,
+                                       unsigned int width,
+                                       unsigned int height,
+                                       unsigned int pitch,
+                                       PixelFormat format,
+                                       const void* buffer) = 0;
+
+#if ORTHANC_SANDBOXED == 0
+    virtual void WriteToFileInternal(const std::string& path,
+                                     unsigned int width,
+                                     unsigned int height,
+                                     unsigned int pitch,
+                                     PixelFormat format,
+                                     const void* buffer);
+#endif
+
+  public:
+    virtual ~IImageWriter()
+    {
+    }
+
+    virtual void WriteToMemory(std::string& compressed,
+                               const ImageAccessor& accessor)
+    {
+      WriteToMemoryInternal(compressed, accessor.GetWidth(), accessor.GetHeight(),
+                            accessor.GetPitch(), accessor.GetFormat(), accessor.GetConstBuffer());
+    }
+
+#if ORTHANC_SANDBOXED == 0
+    virtual void WriteToFile(const std::string& path,
+                             const ImageAccessor& accessor)
+    {
+      WriteToFileInternal(path, accessor.GetWidth(), accessor.GetHeight(),
+                          accessor.GetPitch(), accessor.GetFormat(), accessor.GetConstBuffer());
+    }
+#endif
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/Image.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,61 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "Image.h"
+
+#include "ImageProcessing.h"
+
+#include <memory>
+
+namespace Orthanc
+{
+  Image::Image(PixelFormat format,
+               unsigned int width,
+               unsigned int height,
+               bool forceMinimalPitch) :
+    image_(format, width, height, forceMinimalPitch)
+  {
+    ImageAccessor accessor;
+    image_.GetWriteableAccessor(accessor);
+    
+    AssignWritable(format, width, height, accessor.GetPitch(), accessor.GetBuffer());
+  }
+
+
+  Image* Image::Clone(const ImageAccessor& source)
+  {
+    std::auto_ptr<Image> target(new Image(source.GetFormat(), source.GetWidth(), source.GetHeight(), false));
+    ImageProcessing::Copy(*target, source);
+    return target.release();
+  }
+}
--- a/Core/Images/Image.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/Image.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -45,11 +46,9 @@
   public:
     Image(PixelFormat format,
           unsigned int width,
-          unsigned int height) :
-      image_(format, width, height)
-    {
-      ImageAccessor accessor = image_.GetAccessor();
-      AssignWritable(format, width, height, accessor.GetPitch(), accessor.GetBuffer());
-    }
+          unsigned int height,
+          bool forceMinimalPitch);
+
+    static Image* Clone(const ImageAccessor& source);
   };
 }
--- a/Core/Images/ImageAccessor.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/ImageAccessor.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -65,7 +66,7 @@
 
       for (unsigned int x = 0; x < source.GetWidth(); x++, p++)
       {
-        s += boost::lexical_cast<std::string>(static_cast<int>(*p)) + " ";
+        s += boost::lexical_cast<std::string>(static_cast<double>(*p)) + " ";
       }
 
       target.AddChunk(s);
@@ -121,7 +122,7 @@
   {
     if (buffer_ != NULL)
     {
-      return reinterpret_cast<const uint8_t*>(buffer_) + y * pitch_;
+      return buffer_ + y * pitch_;
     }
     else
     {
@@ -143,7 +144,7 @@
 
     if (buffer_ != NULL)
     {
-      return reinterpret_cast<uint8_t*>(buffer_) + y * pitch_;
+      return buffer_ + y * pitch_;
     }
     else
     {
@@ -174,9 +175,12 @@
     width_ = width;
     height_ = height;
     pitch_ = pitch;
-    buffer_ = const_cast<void*>(buffer);
+    buffer_ = reinterpret_cast<uint8_t*>(const_cast<void*>(buffer));
 
-    assert(GetBytesPerPixel() * width_ <= pitch_);
+    if (GetBytesPerPixel() * width_ > pitch_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
   }
 
 
@@ -191,9 +195,25 @@
     width_ = width;
     height_ = height;
     pitch_ = pitch;
-    buffer_ = buffer;
+    buffer_ = reinterpret_cast<uint8_t*>(buffer);
+
+    if (GetBytesPerPixel() * width_ > pitch_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
 
-    assert(GetBytesPerPixel() * width_ <= pitch_);
+  void ImageAccessor::GetWriteableAccessor(ImageAccessor& target) const
+  {
+    if (readOnly_)
+    {
+      throw OrthancException(ErrorCode_ReadOnly);
+    }
+    else
+    {
+      target.AssignWritable(format_, width_, height_, pitch_, buffer_);
+    }
   }
 
 
@@ -211,10 +231,22 @@
         ToMatlabStringInternal<uint16_t>(buffer, *this);
         break;
 
+      case PixelFormat_Grayscale32:
+        ToMatlabStringInternal<uint32_t>(buffer, *this);
+        break;
+
+      case PixelFormat_Grayscale64:
+        ToMatlabStringInternal<uint64_t>(buffer, *this);
+        break;
+
       case PixelFormat_SignedGrayscale16:
         ToMatlabStringInternal<int16_t>(buffer, *this);
         break;
 
+      case PixelFormat_Float32:
+        ToMatlabStringInternal<float>(buffer, *this);
+        break;
+
       case PixelFormat_RGB24:
         RGB24ToMatlabString(buffer, *this);
         break;
@@ -226,4 +258,58 @@
     buffer.Flatten(target);
   }
 
+
+
+  void ImageAccessor::GetRegion(ImageAccessor& accessor,
+                                unsigned int x,
+                                unsigned int y,
+                                unsigned int width,
+                                unsigned int height) const
+  {
+    if (x + width > width_ ||
+        y + height > height_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    
+    if (width == 0 ||
+        height == 0)
+    {
+      accessor.AssignWritable(format_, 0, 0, 0, NULL);
+    }
+    else
+    {
+      uint8_t* p = (buffer_ + 
+                    y * pitch_ + 
+                    x * GetBytesPerPixel());
+
+      if (readOnly_)
+      {
+        accessor.AssignReadOnly(format_, width, height, pitch_, p);
+      }
+      else
+      {
+        accessor.AssignWritable(format_, width, height, pitch_, p);
+      }
+    }
+  }
+
+
+  void ImageAccessor::SetFormat(PixelFormat format)
+  {
+    if (readOnly_)
+    {
+#if ORTHANC_ENABLE_LOGGING == 1
+      LOG(ERROR) << "Trying to modify the format of a read-only image";
+#endif
+      throw OrthancException(ErrorCode_ReadOnly);
+    }
+
+    if (::Orthanc::GetBytesPerPixel(format) != ::Orthanc::GetBytesPerPixel(format_))
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat);
+    }
+
+    format_ = format;
+  }
 }
--- a/Core/Images/ImageAccessor.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/ImageAccessor.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -35,18 +36,40 @@
 #include "../Enumerations.h"
 
 #include <string>
+#include <stdint.h>
+#include <boost/noncopyable.hpp>
 
 namespace Orthanc
 {
-  class ImageAccessor
+  class ImageAccessor : public boost::noncopyable
   {
   private:
+    template <Orthanc::PixelFormat Format>
+    friend struct ImageTraits;
+    
     bool readOnly_;
     PixelFormat format_;
     unsigned int width_;
     unsigned int height_;
     unsigned int pitch_;
-    void *buffer_;
+    uint8_t *buffer_;
+
+    template <typename T>
+    const T& GetPixelUnchecked(unsigned int x,
+                               unsigned int y) const
+    {
+      const uint8_t* row = reinterpret_cast<const uint8_t*>(buffer_) + y * pitch_;
+      return reinterpret_cast<const T*>(row) [x];
+    }
+
+
+    template <typename T>
+    T& GetPixelUnchecked(unsigned int x,
+                         unsigned int y)
+    {
+      uint8_t* row = reinterpret_cast<uint8_t*>(buffer_) + y * pitch_;
+      return reinterpret_cast<T*>(row) [x];
+    }
 
   public:
     ImageAccessor()
@@ -112,12 +135,27 @@
                         unsigned int pitch,
                         const void *buffer);
 
+    void GetReadOnlyAccessor(ImageAccessor& target) const
+    {
+      target.AssignReadOnly(format_, width_, height_, pitch_, buffer_);
+    }
+
     void AssignWritable(PixelFormat format,
                         unsigned int width,
                         unsigned int height,
                         unsigned int pitch,
                         void *buffer);
 
+    void GetWriteableAccessor(ImageAccessor& target) const;
+
     void ToMatlabString(std::string& target) const; 
+
+    void GetRegion(ImageAccessor& accessor,
+                   unsigned int x,
+                   unsigned int y,
+                   unsigned int width,
+                   unsigned int height) const;
+
+    void SetFormat(PixelFormat format);
   };
 }
--- a/Core/Images/ImageBuffer.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/ImageBuffer.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -87,7 +88,9 @@
 
   ImageBuffer::ImageBuffer(PixelFormat format,
                            unsigned int width,
-                           unsigned int height)
+                           unsigned int height,
+                           bool forceMinimalPitch) :
+    forceMinimalPitch_(forceMinimalPitch)
   {
     Initialize();
     SetWidth(width);
@@ -137,34 +140,18 @@
     }
   }
 
-
-  ImageAccessor ImageBuffer::GetAccessor()
-  {
-    Allocate();
-
-    ImageAccessor accessor;
-    accessor.AssignWritable(format_, width_, height_, pitch_, buffer_);
-    return accessor;
-  }
-
-
-  ImageAccessor ImageBuffer::GetConstAccessor()
+  
+  void ImageBuffer::GetReadOnlyAccessor(ImageAccessor& accessor)
   {
     Allocate();
-
-    ImageAccessor accessor;
     accessor.AssignReadOnly(format_, width_, height_, pitch_, buffer_);
-    return accessor;
   }
-
+  
 
-  void ImageBuffer::SetMinimalPitchForced(bool force)
+  void ImageBuffer::GetWriteableAccessor(ImageAccessor& accessor)
   {
-    if (force != forceMinimalPitch_)
-    {
-      changed_ = true;
-      forceMinimalPitch_ = force;
-    }
+    Allocate();
+    accessor.AssignWritable(format_, width_, height_, pitch_, buffer_);
   }
 
 
--- a/Core/Images/ImageBuffer.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/ImageBuffer.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -61,7 +62,8 @@
   public:
     ImageBuffer(PixelFormat format,
                 unsigned int width,
-                unsigned int height);
+                unsigned int height,
+                bool forceMinimalPitch);
 
     ImageBuffer()
     {
@@ -99,17 +101,15 @@
       return ::Orthanc::GetBytesPerPixel(format_);
     }
 
-    ImageAccessor GetAccessor();
+    void GetReadOnlyAccessor(ImageAccessor& accessor);
 
-    ImageAccessor GetConstAccessor();
+    void GetWriteableAccessor(ImageAccessor& accessor);
 
     bool IsMinimalPitchForced() const
     {
       return forceMinimalPitch_;
     }
 
-    void SetMinimalPitchForced(bool force);
-
     void AcquireOwnership(ImageBuffer& other);
   };
 }
--- a/Core/Images/ImageProcessing.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/ImageProcessing.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,7 +34,7 @@
 #include "../PrecompiledHeaders.h"
 #include "ImageProcessing.h"
 
-#include "../OrthancException.h"
+#include "PixelTraits.h"
 
 #include <boost/math/special_functions/round.hpp>
 
@@ -75,9 +76,28 @@
   }
 
 
+  template <typename SourceType>
+  static void ConvertGrayscaleToFloat(ImageAccessor& target,
+                                      const ImageAccessor& source)
+  {
+    assert(sizeof(float) == 4);
+
+    for (unsigned int y = 0; y < source.GetHeight(); y++)
+    {
+      float* t = reinterpret_cast<float*>(target.GetRow(y));
+      const SourceType* s = reinterpret_cast<const SourceType*>(source.GetConstRow(y));
+
+      for (unsigned int x = 0; x < source.GetWidth(); x++, t++, s++)
+      {
+        *t = static_cast<float>(*s);
+      }
+    }
+  }
+
+
   template <typename TargetType>
   static void ConvertColorToGrayscale(ImageAccessor& target,
-                              const ImageAccessor& source)
+                                      const ImageAccessor& source)
   {
     assert(source.GetFormat() == PixelFormat_RGB24);
 
@@ -94,7 +114,7 @@
         // Y = 0.2126 R + 0.7152 G + 0.0722 B
         int32_t v = (2126 * static_cast<int32_t>(s[0]) +
                      7152 * static_cast<int32_t>(s[1]) +
-                     0722 * static_cast<int32_t>(s[2])) / 1000;
+                     0722 * static_cast<int32_t>(s[2])) / 10000;
         
         if (static_cast<int32_t>(v) < static_cast<int32_t>(minValue))
         {
@@ -146,11 +166,13 @@
     minValue = std::numeric_limits<PixelType>::max();
     maxValue = std::numeric_limits<PixelType>::min();
 
+    const unsigned int width = source.GetWidth();
+
     for (unsigned int y = 0; y < source.GetHeight(); y++)
     {
       const PixelType* p = reinterpret_cast<const PixelType*>(source.GetConstRow(y));
 
-      for (unsigned int x = 0; x < source.GetWidth(); x++, p++)
+      for (unsigned int x = 0; x < width; x++, p++)
       {
         if (*p < minValue)
         {
@@ -205,9 +227,10 @@
 
 
 
-  template <typename PixelType>
-  void MultiplyConstantInternal(ImageAccessor& image,
-                                float factor)
+  template <typename PixelType,
+            bool UseRound>
+  static void MultiplyConstantInternal(ImageAccessor& image,
+                                       float factor)
   {
     if (std::abs(factor - 1.0f) <= std::numeric_limits<float>::epsilon())
     {
@@ -216,14 +239,24 @@
 
     const int64_t minValue = std::numeric_limits<PixelType>::min();
     const int64_t maxValue = std::numeric_limits<PixelType>::max();
+    const unsigned int width = image.GetWidth();
 
     for (unsigned int y = 0; y < image.GetHeight(); y++)
     {
       PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
 
-      for (unsigned int x = 0; x < image.GetWidth(); x++, p++)
+      for (unsigned int x = 0; x < width; x++, p++)
       {
-        int64_t v = boost::math::llround(static_cast<float>(*p) * factor);
+        int64_t v;
+        if (UseRound)
+        {
+          // The "round" operation is very costly
+          v = boost::math::llround(static_cast<float>(*p) * factor);
+        }
+        else
+        {
+          v = static_cast<int64_t>(static_cast<float>(*p) * factor);
+        }
 
         if (v > maxValue)
         {
@@ -242,33 +275,44 @@
   }
 
 
-  template <typename PixelType>
-  void ShiftScaleInternal(ImageAccessor& image,
-                          float offset,
-                          float scaling)
+  template <typename PixelType,
+            bool UseRound>
+  static void ShiftScaleInternal(ImageAccessor& image,
+                                 float offset,
+                                 float scaling)
   {
-    const float minValue = static_cast<float>(std::numeric_limits<PixelType>::min());
-    const float maxValue = static_cast<float>(std::numeric_limits<PixelType>::max());
+    const float minFloatValue = static_cast<float>(std::numeric_limits<PixelType>::min());
+    const float maxFloatValue = static_cast<float>(std::numeric_limits<PixelType>::max());
+    const PixelType minPixelValue = std::numeric_limits<PixelType>::min();
+    const PixelType maxPixelValue = std::numeric_limits<PixelType>::max();
 
-    for (unsigned int y = 0; y < image.GetHeight(); y++)
+    const unsigned int height = image.GetHeight();
+    const unsigned int width = image.GetWidth();
+    
+    for (unsigned int y = 0; y < height; y++)
     {
       PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
 
-      for (unsigned int x = 0; x < image.GetWidth(); x++, p++)
+      for (unsigned int x = 0; x < width; x++, p++)
       {
         float v = (static_cast<float>(*p) + offset) * scaling;
 
-        if (v > maxValue)
+        if (v > maxFloatValue)
         {
-          *p = std::numeric_limits<PixelType>::max();
+          *p = maxPixelValue;
         }
-        else if (v < minValue)
+        else if (v < minFloatValue)
         {
-          *p = std::numeric_limits<PixelType>::min();
+          *p = minPixelValue;
+        }
+        else if (UseRound)
+        {
+          // The "round" operation is very costly
+          *p = static_cast<PixelType>(boost::math::iround(v));
         }
         else
         {
-          *p = static_cast<PixelType>(boost::math::iround(v));
+          *p = static_cast<PixelType>(v);
         }
       }
     }
@@ -378,6 +422,34 @@
       return;
     }
 
+    if (target.GetFormat() == PixelFormat_Float32 &&
+        source.GetFormat() == PixelFormat_Grayscale8)
+    {
+      ConvertGrayscaleToFloat<uint8_t>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Float32 &&
+        source.GetFormat() == PixelFormat_Grayscale16)
+    {
+      ConvertGrayscaleToFloat<uint16_t>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Float32 &&
+        source.GetFormat() == PixelFormat_Grayscale32)
+    {
+      ConvertGrayscaleToFloat<uint32_t>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Float32 &&
+        source.GetFormat() == PixelFormat_SignedGrayscale16)
+    {
+      ConvertGrayscaleToFloat<int16_t>(target, source);
+      return;
+    }
+
     if (target.GetFormat() == PixelFormat_Grayscale8 &&
         source.GetFormat() == PixelFormat_RGBA32)
     {
@@ -397,6 +469,25 @@
       return;
     }
 
+    if (target.GetFormat() == PixelFormat_Grayscale8 &&
+        source.GetFormat() == PixelFormat_BGRA32)
+    {
+      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < source.GetWidth(); x++, q++)
+        {
+          *q = static_cast<uint8_t>((2126 * static_cast<uint32_t>(p[2]) +
+                                     7152 * static_cast<uint32_t>(p[1]) +
+                                     0722 * static_cast<uint32_t>(p[0])) / 10000);
+          p += 4;
+        }
+      }
+
+      return;
+    }
+
     if (target.GetFormat() == PixelFormat_RGB24 &&
         source.GetFormat() == PixelFormat_RGBA32)
     {
@@ -417,6 +508,26 @@
       return;
     }
 
+    if (target.GetFormat() == PixelFormat_RGB24 &&
+        source.GetFormat() == PixelFormat_BGRA32)
+    {
+      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        {
+          q[0] = p[2];
+          q[1] = p[1];
+          q[2] = p[0];
+          p += 4;
+          q += 3;
+        }
+      }
+
+      return;
+    }
+
     if (target.GetFormat() == PixelFormat_RGBA32 &&
         source.GetFormat() == PixelFormat_RGB24)
     {
@@ -438,6 +549,146 @@
       return;
     }
 
+    if (target.GetFormat() == PixelFormat_RGB24 &&
+        source.GetFormat() == PixelFormat_Grayscale8)
+    {
+      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        {
+          q[0] = *p;
+          q[1] = *p;
+          q[2] = *p;
+          p += 1;
+          q += 3;
+        }
+      }
+
+      return;
+    }
+
+    if ((target.GetFormat() == PixelFormat_RGBA32 ||
+         target.GetFormat() == PixelFormat_BGRA32) &&
+        source.GetFormat() == PixelFormat_Grayscale8)
+    {
+      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        {
+          q[0] = *p;
+          q[1] = *p;
+          q[2] = *p;
+          q[3] = 255;
+          p += 1;
+          q += 4;
+        }
+      }
+
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_BGRA32 &&
+        source.GetFormat() == PixelFormat_Grayscale16)
+    {
+      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      {
+        const uint16_t* p = reinterpret_cast<const uint16_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        {
+          uint8_t value = (*p < 256 ? *p : 255);
+          q[0] = value;
+          q[1] = value;
+          q[2] = value;
+          q[3] = 255;
+          p += 1;
+          q += 4;
+        }
+      }
+
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_BGRA32 &&
+        source.GetFormat() == PixelFormat_SignedGrayscale16)
+    {
+      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      {
+        const int16_t* p = reinterpret_cast<const int16_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        {
+          uint8_t value;
+          if (*p < 0)
+          {
+            value = 0;
+          }
+          else if (*p > 255)
+          {
+            value = 255;
+          }
+          else
+          {
+            value = static_cast<uint8_t>(*p);
+          }
+
+          q[0] = value;
+          q[1] = value;
+          q[2] = value;
+          q[3] = 255;
+          p += 1;
+          q += 4;
+        }
+      }
+
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_BGRA32 &&
+        source.GetFormat() == PixelFormat_RGB24)
+    {
+      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        {
+          q[0] = p[2];
+          q[1] = p[1];
+          q[2] = p[0];
+          q[3] = 255;
+          p += 3;
+          q += 4;
+        }
+      }
+
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_RGB24 &&
+        source.GetFormat() == PixelFormat_RGB48)
+    {
+      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      {
+        const uint16_t* p = reinterpret_cast<const uint16_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        {
+          q[0] = p[0] >> 8;
+          q[1] = p[1] >> 8;
+          q[2] = p[2] >> 8;
+          p += 3;
+          q += 3;
+        }
+      }
+
+      return;
+    }
+
     throw OrthancException(ErrorCode_NotImplemented);
   }
 
@@ -449,15 +700,56 @@
     switch (image.GetFormat())
     {
       case PixelFormat_Grayscale8:
-        SetInternal<uint8_t>(image, value);
+        memset(image.GetBuffer(), static_cast<uint8_t>(value), image.GetPitch() * image.GetHeight());
         return;
 
       case PixelFormat_Grayscale16:
-        SetInternal<uint16_t>(image, value);
+        if (value == 0)
+        {
+          memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight());
+        }
+        else
+        {
+          SetInternal<uint16_t>(image, value);
+        }
+        return;
+
+      case PixelFormat_Grayscale32:
+        if (value == 0)
+        {
+          memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight());
+        }
+        else
+        {
+          SetInternal<uint32_t>(image, value);
+        }
+        return;
+
+      case PixelFormat_Grayscale64:
+        if (value == 0)
+        {
+          memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight());
+        }
+        else
+        {
+          SetInternal<uint64_t>(image, value);
+        }
         return;
 
       case PixelFormat_SignedGrayscale16:
-        SetInternal<int16_t>(image, value);
+        if (value == 0)
+        {
+          memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight());
+        }
+        else
+        {
+          SetInternal<int16_t>(image, value);
+        }
+        return;
+
+      case PixelFormat_Float32:
+        assert(sizeof(float) == 4);
+        SetInternal<float>(image, value);
         return;
 
       default:
@@ -466,6 +758,61 @@
   }
 
 
+  void ImageProcessing::Set(ImageAccessor& image,
+                            uint8_t red,
+                            uint8_t green,
+                            uint8_t blue,
+                            uint8_t alpha)
+  {
+    uint8_t p[4];
+    unsigned int size;
+
+    switch (image.GetFormat())
+    {
+      case PixelFormat_RGBA32:
+        p[0] = red;
+        p[1] = green;
+        p[2] = blue;
+        p[3] = alpha;
+        size = 4;
+        break;
+
+      case PixelFormat_BGRA32:
+        p[0] = blue;
+        p[1] = green;
+        p[2] = red;
+        p[3] = alpha;
+        size = 4;
+        break;
+
+      case PixelFormat_RGB24:
+        p[0] = red;
+        p[1] = green;
+        p[2] = blue;
+        size = 3;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }    
+
+    for (unsigned int y = 0; y < image.GetHeight(); y++)
+    {
+      uint8_t* q = reinterpret_cast<uint8_t*>(image.GetRow(y));
+
+      for (unsigned int x = 0; x < image.GetWidth(); x++)
+      {
+        for (unsigned int i = 0; i < size; i++)
+        {
+          q[i] = p[i];
+        }
+
+        q += size;
+      }
+    }
+  }
+
+
   void ImageProcessing::ShiftRight(ImageAccessor& image,
                                    unsigned int shift)
   {
@@ -481,9 +828,9 @@
   }
 
 
-  void ImageProcessing::GetMinMaxValue(int64_t& minValue,
-                                       int64_t& maxValue,
-                                       const ImageAccessor& image)
+  void ImageProcessing::GetMinMaxIntegerValue(int64_t& minValue,
+                                              int64_t& maxValue,
+                                              const ImageAccessor& image)
   {
     switch (image.GetFormat())
     {
@@ -505,6 +852,15 @@
         break;
       }
 
+      case PixelFormat_Grayscale32:
+      {
+        uint32_t a, b;
+        GetMinMaxValueInternal<uint32_t>(a, b, image);
+        minValue = a;
+        maxValue = b;
+        break;
+      }
+
       case PixelFormat_SignedGrayscale16:
       {
         int16_t a, b;
@@ -520,6 +876,28 @@
   }
 
 
+  void ImageProcessing::GetMinMaxFloatValue(float& minValue,
+                                            float& maxValue,
+                                            const ImageAccessor& image)
+  {
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Float32:
+      {
+        assert(sizeof(float) == 32);
+        float a, b;
+        GetMinMaxValueInternal<float>(a, b, image);
+        minValue = a;
+        maxValue = b;
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
 
   void ImageProcessing::AddConstant(ImageAccessor& image,
                                     int64_t value)
@@ -545,20 +923,42 @@
 
 
   void ImageProcessing::MultiplyConstant(ImageAccessor& image,
-                                         float factor)
+                                         float factor,
+                                         bool useRound)
   {
     switch (image.GetFormat())
     {
       case PixelFormat_Grayscale8:
-        MultiplyConstantInternal<uint8_t>(image, factor);
+        if (useRound)
+        {
+          MultiplyConstantInternal<uint8_t, true>(image, factor);
+        }
+        else
+        {
+          MultiplyConstantInternal<uint8_t, false>(image, factor);
+        }
         return;
 
       case PixelFormat_Grayscale16:
-        MultiplyConstantInternal<uint16_t>(image, factor);
+        if (useRound)
+        {
+          MultiplyConstantInternal<uint16_t, true>(image, factor);
+        }
+        else
+        {
+          MultiplyConstantInternal<uint16_t, false>(image, factor);
+        }
         return;
 
       case PixelFormat_SignedGrayscale16:
-        MultiplyConstantInternal<int16_t>(image, factor);
+        if (useRound)
+        {
+          MultiplyConstantInternal<int16_t, true>(image, factor);
+        }
+        else
+        {
+          MultiplyConstantInternal<int16_t, false>(image, factor);
+        }
         return;
 
       default:
@@ -569,24 +969,302 @@
 
   void ImageProcessing::ShiftScale(ImageAccessor& image,
                                    float offset,
-                                   float scaling)
+                                   float scaling,
+                                   bool useRound)
   {
     switch (image.GetFormat())
     {
       case PixelFormat_Grayscale8:
-        ShiftScaleInternal<uint8_t>(image, offset, scaling);
+        if (useRound)
+        {
+          ShiftScaleInternal<uint8_t, true>(image, offset, scaling);
+        }
+        else
+        {
+          ShiftScaleInternal<uint8_t, false>(image, offset, scaling);
+        }
         return;
 
       case PixelFormat_Grayscale16:
-        ShiftScaleInternal<uint16_t>(image, offset, scaling);
+        if (useRound)
+        {
+          ShiftScaleInternal<uint16_t, true>(image, offset, scaling);
+        }
+        else
+        {
+          ShiftScaleInternal<uint16_t, false>(image, offset, scaling);
+        }
         return;
 
       case PixelFormat_SignedGrayscale16:
-        ShiftScaleInternal<int16_t>(image, offset, scaling);
+        if (useRound)
+        {
+          ShiftScaleInternal<int16_t, true>(image, offset, scaling);
+        }
+        else
+        {
+          ShiftScaleInternal<int16_t, false>(image, offset, scaling);
+        }
         return;
 
       default:
         throw OrthancException(ErrorCode_NotImplemented);
     }
   }
+
+
+  void ImageProcessing::Invert(ImageAccessor& image)
+  {
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+      {
+        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++)
+          {
+            *p = 255 - (*p);
+          }
+        }
+        
+        return;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }   
+  }
+
+
+
+  namespace
+  {
+    template <Orthanc::PixelFormat Format>
+    class BresenhamPixelWriter
+    {
+    private:
+      typedef typename PixelTraits<Format>::PixelType  PixelType;
+    
+      Orthanc::ImageAccessor&  image_;
+      PixelType                value_;
+
+      void PlotLineLow(int x0,
+                       int y0,
+                       int x1,
+                       int y1)
+      {
+        int dx = x1 - x0;
+        int dy = y1 - y0;
+        int yi = 1;
+
+        if (dy < 0)
+        {
+          yi = -1;
+          dy = -dy;
+        }
+
+        int d = 2 * dy - dx;
+        int y = y0;
+
+        for (int x = x0; x <= x1; x++)
+        {
+          Write(x, y);
+          
+          if (d > 0)
+          {
+            y = y + yi;
+            d = d - 2 * dx;
+          }
+      
+          d = d + 2*dy;
+        }
+      }
+      
+      void PlotLineHigh(int x0,
+                        int y0,
+                        int x1,
+                        int y1)
+      {
+        int dx = x1 - x0;
+        int dy = y1 - y0;
+        int xi = 1;
+    
+        if (dx < 0)
+        {
+          xi = -1;
+          dx = -dx;
+        }
+    
+        int d = 2 * dx - dy;
+        int x = x0;
+
+        for (int y = y0; y <= y1; y++)
+        {
+          Write(x, y);
+          
+          if (d > 0)
+          {
+            x = x + xi;
+            d = d - 2 * dy;
+          }
+      
+          d = d + 2 * dx;
+        }
+      }
+
+    public:
+      BresenhamPixelWriter(Orthanc::ImageAccessor& image,
+                           int64_t value) :
+        image_(image),
+        value_(PixelTraits<Format>::IntegerToPixel(value))
+      {
+      }
+
+      BresenhamPixelWriter(Orthanc::ImageAccessor& image,
+                           const PixelType& value) :
+        image_(image),
+        value_(value)
+      {
+      }
+
+      void Write(int x,
+                 int y)
+      {
+        if (x >= 0 &&
+            y >= 0 &&
+            static_cast<unsigned int>(x) < image_.GetWidth() &&
+            static_cast<unsigned int>(y) < image_.GetHeight())
+        {
+          PixelType* p = reinterpret_cast<PixelType*>(image_.GetRow(y));
+          p[x] = value_;
+        }
+      }
+
+      void DrawSegment(int x0,
+                       int y0,
+                       int x1,
+                       int y1)
+      {
+        // This is an implementation of Bresenham's line algorithm
+        // https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm#All_cases
+    
+        if (abs(y1 - y0) < abs(x1 - x0))
+        {
+          if (x0 > x1)
+          {
+            PlotLineLow(x1, y1, x0, y0);
+          }
+          else
+          {
+            PlotLineLow(x0, y0, x1, y1);
+          }
+        }
+        else
+        {
+          if (y0 > y1)
+          {
+            PlotLineHigh(x1, y1, x0, y0);
+          }
+          else
+          {
+            PlotLineHigh(x0, y0, x1, y1);
+          }
+        }
+      }
+    };
+  }
+
+  
+  void ImageProcessing::DrawLineSegment(ImageAccessor& image,
+                                        int x0,
+                                        int y0,
+                                        int x1,
+                                        int y1,
+                                        int64_t value)
+  {
+    switch (image.GetFormat())
+    {       
+      case Orthanc::PixelFormat_Grayscale8:
+      {
+        BresenhamPixelWriter<Orthanc::PixelFormat_Grayscale8> writer(image, value);
+        writer.DrawSegment(x0, y0, x1, y1);
+        break;
+      }
+
+      case Orthanc::PixelFormat_Grayscale16:
+      {
+        BresenhamPixelWriter<Orthanc::PixelFormat_Grayscale16> writer(image, value);
+        writer.DrawSegment(x0, y0, x1, y1);
+        break;
+      }
+
+      case Orthanc::PixelFormat_SignedGrayscale16:
+      {
+        BresenhamPixelWriter<Orthanc::PixelFormat_SignedGrayscale16> writer(image, value);
+        writer.DrawSegment(x0, y0, x1, y1);
+        break;
+      }
+        
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+
+  
+  void ImageProcessing::DrawLineSegment(ImageAccessor& image,
+                                        int x0,
+                                        int y0,
+                                        int x1,
+                                        int y1,
+                                        uint8_t red,
+                                        uint8_t green,
+                                        uint8_t blue,
+                                        uint8_t alpha)
+  {
+    switch (image.GetFormat())
+    {
+      case Orthanc::PixelFormat_BGRA32:
+      {
+        PixelTraits<Orthanc::PixelFormat_BGRA32>::PixelType pixel;
+        pixel.red_ = red;
+        pixel.green_ = green;
+        pixel.blue_ = blue;
+        pixel.alpha_ = alpha;
+
+        BresenhamPixelWriter<Orthanc::PixelFormat_BGRA32> writer(image, pixel);
+        writer.DrawSegment(x0, y0, x1, y1);
+        break;
+      }
+        
+      case Orthanc::PixelFormat_RGBA32:
+      {
+        PixelTraits<Orthanc::PixelFormat_RGBA32>::PixelType pixel;
+        pixel.red_ = red;
+        pixel.green_ = green;
+        pixel.blue_ = blue;
+        pixel.alpha_ = alpha;
+
+        BresenhamPixelWriter<Orthanc::PixelFormat_RGBA32> writer(image, pixel);
+        writer.DrawSegment(x0, y0, x1, y1);
+        break;
+      }
+        
+      case Orthanc::PixelFormat_RGB24:
+      {
+        PixelTraits<Orthanc::PixelFormat_RGB24>::PixelType pixel;
+        pixel.red_ = red;
+        pixel.green_ = green;
+        pixel.blue_ = blue;
+
+        BresenhamPixelWriter<Orthanc::PixelFormat_RGB24> writer(image, pixel);
+        writer.DrawSegment(x0, y0, x1, y1);
+        break;
+      }
+        
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
 }
--- a/Core/Images/ImageProcessing.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/ImageProcessing.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -38,33 +39,65 @@
 
 namespace Orthanc
 {
-  class ImageProcessing
+  namespace ImageProcessing
   {
-  public:
-    static void Copy(ImageAccessor& target,
-                     const ImageAccessor& source);
+    void Copy(ImageAccessor& target,
+              const ImageAccessor& source);
+
+    void Convert(ImageAccessor& target,
+                 const ImageAccessor& source);
+
+    void Set(ImageAccessor& image,
+             int64_t value);
 
-    static void Convert(ImageAccessor& target,
-                        const ImageAccessor& source);
+    void Set(ImageAccessor& image,
+             uint8_t red,
+             uint8_t green,
+             uint8_t blue,
+             uint8_t alpha);
 
-    static void Set(ImageAccessor& image,
-                    int64_t value);
+    void ShiftRight(ImageAccessor& target,
+                    unsigned int shift);
 
-    static void ShiftRight(ImageAccessor& target,
-                           unsigned int shift);
-
-    static void GetMinMaxValue(int64_t& minValue,
+    void GetMinMaxIntegerValue(int64_t& minValue,
                                int64_t& maxValue,
                                const ImageAccessor& image);
 
-    static void AddConstant(ImageAccessor& image,
-                            int64_t value);
+    void GetMinMaxFloatValue(float& minValue,
+                             float& maxValue,
+                             const ImageAccessor& image);
+
+    void AddConstant(ImageAccessor& image,
+                     int64_t value);
 
-    static void MultiplyConstant(ImageAccessor& image,
-                                 float factor);
+    // "useRound" is expensive
+    void MultiplyConstant(ImageAccessor& image,
+                          float factor,
+                          bool useRound);
+
+    // "useRound" is expensive
+    void ShiftScale(ImageAccessor& image,
+                    float offset,
+                    float scaling,
+                    bool useRound);
 
-    static void ShiftScale(ImageAccessor& image,
-                           float offset,
-                           float scaling);
-  };
+    void Invert(ImageAccessor& image);
+
+    void DrawLineSegment(ImageAccessor& image,
+                         int x0,
+                         int y0,
+                         int x1,
+                         int y1,
+                         int64_t value);
+
+    void DrawLineSegment(ImageAccessor& image,
+                         int x0,
+                         int y0,
+                         int x1,
+                         int y1,
+                         uint8_t red,
+                         uint8_t green,
+                         uint8_t blue,
+                         uint8_t alpha);
+  }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/ImageTraits.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,89 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ImageAccessor.h"
+#include "PixelTraits.h"
+
+#include <cassert>
+
+namespace Orthanc
+{
+  template <PixelFormat Format>
+  struct ImageTraits
+  {
+    typedef ::Orthanc::PixelTraits<Format>    PixelTraits;
+    typedef typename PixelTraits::PixelType   PixelType;
+
+    static PixelFormat GetPixelFormat()
+    {
+      return Format;
+    }
+
+    static void GetPixel(PixelType& target,
+                         const ImageAccessor& image,
+                         unsigned int x,
+                         unsigned int y)
+    {
+      assert(x < image.GetWidth() && y < image.GetHeight());
+      PixelTraits::Copy(target, image.GetPixelUnchecked<PixelType>(x, y));
+    }
+
+    static void SetPixel(ImageAccessor& image,
+                         const PixelType& value,
+                         unsigned int x,
+                         unsigned int y)
+    {
+      assert(x < image.GetWidth() && y < image.GetHeight());
+      PixelTraits::Copy(image.GetPixelUnchecked<PixelType>(x, y), value);
+    }
+
+    static float GetFloatPixel(const ImageAccessor& image,
+                               unsigned int x,
+                               unsigned int y)
+    {
+      assert(x < image.GetWidth() && y < image.GetHeight());
+      return PixelTraits::PixelToFloat(image.GetPixelUnchecked<PixelType>(x, y));
+    }
+
+    static void SetFloatPixel(ImageAccessor& image,
+                              float value,
+                              unsigned int x,
+                              unsigned int y)
+    {
+      assert(x < image.GetWidth() && y < image.GetHeight());
+      PixelTraits::FloatToPixel(image.GetPixelUnchecked<PixelType>(x, y), value);
+    }
+  };
+}
--- a/Core/Images/JpegErrorManager.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/JpegErrorManager.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Images/JpegErrorManager.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/JpegErrorManager.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -31,6 +32,14 @@
 
 #pragma once
 
+#if !defined(ORTHANC_ENABLE_JPEG)
+#  error The macro ORTHANC_ENABLE_JPEG must be defined
+#endif
+
+#if ORTHANC_ENABLE_JPEG != 1
+#  error JPEG support must be enabled to include this file
+#endif
+
 #include <string.h>
 #include <stdio.h>
 #include <jpeglib.h>
--- a/Core/Images/JpegReader.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/JpegReader.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -37,13 +38,21 @@
 #include "../OrthancException.h"
 #include "../Logging.h"
 
+#if ORTHANC_SANDBOXED == 0
+#  include "../SystemToolbox.h"
+#endif
+
+
 namespace Orthanc
 {
   static void Uncompress(struct jpeg_decompress_struct& cinfo,
                          std::string& content,
                          ImageAccessor& accessor)
   {
-    jpeg_read_header(&cinfo, TRUE);
+    // The "static_cast" is necessary on OS X:
+    // https://github.com/simonfuhrmann/mve/issues/371
+    jpeg_read_header(&cinfo, static_cast<boolean>(true));
+
     jpeg_start_decompress(&cinfo);
 
     PixelFormat format;
@@ -93,9 +102,10 @@
   }
 
 
-  void JpegReader::ReadFromFile(const char* filename)
+#if ORTHANC_SANDBOXED == 0
+  void JpegReader::ReadFromFile(const std::string& filename)
   {
-    FILE* fp = fopen(filename, "rb");
+    FILE* fp = SystemToolbox::OpenFile(filename, FileMode_ReadBinary);
     if (!fp)
     {
       throw OrthancException(ErrorCode_InexistentFile);
@@ -111,7 +121,7 @@
     {
       jpeg_destroy_decompress(&cinfo);
       fclose(fp);
-      LOG(ERROR) << "Error during JPEG encoding: " << jerr.GetMessage();
+      LOG(ERROR) << "Error during JPEG decoding: " << jerr.GetMessage();
       throw OrthancException(ErrorCode_InternalError);
     }
 
@@ -134,6 +144,7 @@
     jpeg_destroy_decompress(&cinfo);
     fclose(fp);
   }
+#endif
 
 
   void JpegReader::ReadFromMemory(const void* buffer,
@@ -148,7 +159,7 @@
     if (setjmp(jerr.GetJumpBuffer())) 
     {
       jpeg_destroy_decompress(&cinfo);
-      LOG(ERROR) << "Error during JPEG encoding: " << jerr.GetMessage();
+      LOG(ERROR) << "Error during JPEG decoding: " << jerr.GetMessage();
       throw OrthancException(ErrorCode_InternalError);
     }
 
--- a/Core/Images/JpegReader.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/JpegReader.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,6 +33,18 @@
 
 #pragma once
 
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_JPEG)
+#  error The macro ORTHANC_ENABLE_JPEG must be defined
+#endif
+
+#if ORTHANC_ENABLE_JPEG != 1
+#  error JPEG support must be enabled to include this file
+#endif
+
 #include "ImageAccessor.h"
 
 #include <string>
@@ -44,12 +57,9 @@
     std::string  content_;
 
   public:
-    void ReadFromFile(const char* filename);
-
-    void ReadFromFile(const std::string& filename)
-    {
-      ReadFromFile(filename.c_str());
-    }
+#if ORTHANC_SANDBOXED == 0
+    void ReadFromFile(const std::string& filename);
+#endif
 
     void ReadFromMemory(const void* buffer,
                         size_t size);
--- a/Core/Images/JpegWriter.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/JpegWriter.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -35,9 +36,13 @@
 
 #include "../OrthancException.h"
 #include "../Logging.h"
-
 #include "JpegErrorManager.h"
 
+#if ORTHANC_SANDBOXED == 0
+#  include "../SystemToolbox.h"
+#endif
+
+#include <stdlib.h>
 #include <vector>
 
 namespace Orthanc
@@ -91,8 +96,12 @@
     }
 
     jpeg_set_defaults(&cinfo);
-    jpeg_set_quality(&cinfo, quality, TRUE);
-    jpeg_start_compress(&cinfo, TRUE);
+
+    // The "static_cast" is necessary on OS X:
+    // https://github.com/simonfuhrmann/mve/issues/371
+    jpeg_set_quality(&cinfo, quality, static_cast<boolean>(true));
+    jpeg_start_compress(&cinfo, static_cast<boolean>(true));
+    
     jpeg_write_scanlines(&cinfo, &lines[0], height);
     jpeg_finish_compress(&cinfo);
     jpeg_destroy_compress(&cinfo);
@@ -101,7 +110,7 @@
 
   void JpegWriter::SetQuality(uint8_t quality)
   {
-    if (quality <= 0 || quality > 100)
+    if (quality == 0 || quality > 100)
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -110,17 +119,18 @@
   }
 
 
-  void JpegWriter::WriteToFile(const char* filename,
-                               unsigned int width,
-                               unsigned int height,
-                               unsigned int pitch,
-                               PixelFormat format,
-                               const void* buffer)
+#if ORTHANC_SANDBOXED == 0
+  void JpegWriter::WriteToFileInternal(const std::string& filename,
+                                       unsigned int width,
+                                       unsigned int height,
+                                       unsigned int pitch,
+                                       PixelFormat format,
+                                       const void* buffer)
   {
-    FILE* fp = fopen(filename, "wb");
+    FILE* fp = SystemToolbox::OpenFile(filename, FileMode_WriteBinary);
     if (fp == NULL)
     {
-      throw OrthancException(ErrorCode_FullStorage);
+      throw OrthancException(ErrorCode_CannotWriteFile);
     }
 
     std::vector<uint8_t*> lines;
@@ -153,14 +163,15 @@
 
     fclose(fp);
   }
+#endif
 
 
-  void JpegWriter::WriteToMemory(std::string& jpeg,
-                                 unsigned int width,
-                                 unsigned int height,
-                                 unsigned int pitch,
-                                 PixelFormat format,
-                                 const void* buffer)
+  void JpegWriter::WriteToMemoryInternal(std::string& jpeg,
+                                         unsigned int width,
+                                         unsigned int height,
+                                         unsigned int pitch,
+                                         PixelFormat format,
+                                         const void* buffer)
   {
     std::vector<uint8_t*> lines;
     GetLines(lines, height, pitch, format, buffer);
--- a/Core/Images/JpegWriter.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/JpegWriter.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,15 +33,37 @@
 
 #pragma once
 
-#include "ImageAccessor.h"
+#if !defined(ORTHANC_ENABLE_JPEG)
+#  error The macro ORTHANC_ENABLE_JPEG must be defined
+#endif
 
-#include <string>
-#include <stdint.h>
+#if ORTHANC_ENABLE_JPEG != 1
+#  error JPEG support must be enabled to include this file
+#endif
+
+#include "IImageWriter.h"
 
 namespace Orthanc
 {
-  class JpegWriter
+  class JpegWriter : public IImageWriter
   {
+  protected:
+#if ORTHANC_SANDBOXED == 0
+    virtual void WriteToFileInternal(const std::string& filename,
+                                     unsigned int width,
+                                     unsigned int height,
+                                     unsigned int pitch,
+                                     PixelFormat format,
+                                     const void* buffer);
+#endif
+
+    virtual void WriteToMemoryInternal(std::string& jpeg,
+                                       unsigned int width,
+                                       unsigned int height,
+                                       unsigned int pitch,
+                                       PixelFormat format,
+                                       const void* buffer);
+
   private:
     uint8_t  quality_;
 
@@ -55,33 +78,5 @@
     {
       return quality_;
     }
-
-    void WriteToFile(const char* filename,
-                     unsigned int width,
-                     unsigned int height,
-                     unsigned int pitch,
-                     PixelFormat format,
-                     const void* buffer);
-
-    void WriteToMemory(std::string& jpeg,
-                       unsigned int width,
-                       unsigned int height,
-                       unsigned int pitch,
-                       PixelFormat format,
-                       const void* buffer);
-
-    void WriteToFile(const char* filename,
-                     const ImageAccessor& accessor)
-    {
-      WriteToFile(filename, accessor.GetWidth(), accessor.GetHeight(),
-                  accessor.GetPitch(), accessor.GetFormat(), accessor.GetConstBuffer());
-    }
-
-    void WriteToMemory(std::string& jpeg,
-                       const ImageAccessor& accessor)
-    {
-      WriteToMemory(jpeg, accessor.GetWidth(), accessor.GetHeight(),
-                    accessor.GetPitch(), accessor.GetFormat(), accessor.GetConstBuffer());
-    }
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/PamReader.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,247 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "PamReader.h"
+
+#include "../Endianness.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#if ORTHANC_SANDBOXED == 0
+#  include "../SystemToolbox.h"
+#endif
+
+#include <boost/algorithm/string/find.hpp>
+#include <boost/lexical_cast.hpp>
+
+
+namespace Orthanc
+{
+  static void GetPixelFormat(PixelFormat& format,
+                             unsigned int& bytesPerChannel,
+                             const unsigned int& maxValue,
+                             const unsigned int& channelCount,
+                             const std::string& tupleType)
+  {
+    if (tupleType == "GRAYSCALE" &&
+        channelCount == 1)
+    {
+      switch (maxValue)
+      {
+        case 255:
+          format = PixelFormat_Grayscale8;
+          bytesPerChannel = 1;
+          return;
+
+        case 65535:
+          format = PixelFormat_Grayscale16;
+          bytesPerChannel = 2;
+          return;
+
+        default:
+          throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+    else if (tupleType == "RGB" &&
+             channelCount == 3)
+    {
+      switch (maxValue)
+      {
+        case 255:
+          format = PixelFormat_RGB24;
+          bytesPerChannel = 1;
+          return;
+
+        case 65535:
+          format = PixelFormat_RGB48;
+          bytesPerChannel = 2;
+          return;
+
+        default:
+          throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+  
+  typedef std::map<std::string, std::string>  Parameters;
+
+  
+  static std::string LookupStringParameter(const Parameters& parameters,
+                                           const std::string& key)
+  {
+    Parameters::const_iterator found = parameters.find(key);
+
+    if (found == parameters.end())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      return found->second;
+    }
+  }
+  
+
+  static unsigned int LookupIntegerParameter(const Parameters& parameters,
+                                             const std::string& key)
+  {
+    try
+    {
+      int value = boost::lexical_cast<int>(LookupStringParameter(parameters, key));
+
+      if (value < 0)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        return static_cast<unsigned int>(value);
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+  
+
+  void PamReader::ParseContent()
+  {
+    static const std::string headerDelimiter = "ENDHDR\n";
+    
+    boost::iterator_range<std::string::const_iterator> headerRange =
+      boost::algorithm::find_first(content_, headerDelimiter);
+
+    if (!headerRange)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    std::string header(static_cast<const std::string&>(content_).begin(), headerRange.begin());
+
+    std::vector<std::string> lines;
+    Toolbox::TokenizeString(lines, header, '\n');
+
+    if (lines.size() < 2 ||
+        lines.front() != "P7" ||
+        !lines.back().empty())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    Parameters parameters;
+    
+    for (size_t i = 1; i + 1 < lines.size(); i++)
+    {
+      std::vector<std::string> tokens;
+      Toolbox::TokenizeString(tokens, lines[i], ' ');
+
+      if (tokens.size() != 2)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        parameters[tokens[0]] = tokens[1];
+      }
+    }
+
+    const unsigned int width = LookupIntegerParameter(parameters, "WIDTH");
+    const unsigned int height = LookupIntegerParameter(parameters, "HEIGHT");
+    const unsigned int channelCount = LookupIntegerParameter(parameters, "DEPTH");
+    const unsigned int maxValue = LookupIntegerParameter(parameters, "MAXVAL");
+    const std::string tupleType = LookupStringParameter(parameters, "TUPLTYPE");
+
+    unsigned int bytesPerChannel;
+    PixelFormat format;
+    GetPixelFormat(format, bytesPerChannel, maxValue, channelCount, tupleType.c_str());
+
+    unsigned int pitch = width * channelCount * bytesPerChannel;
+
+    if (content_.size() != header.size() + headerDelimiter.size() + pitch * height)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    size_t offset = content_.size() - pitch * height;
+    AssignWritable(format, width, height, pitch, &content_[offset]);
+
+    // Byte swapping if needed
+    if (bytesPerChannel != 1 &&
+        bytesPerChannel != 2)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+    
+    if (Toolbox::DetectEndianness() == Endianness_Little &&
+        bytesPerChannel == 2)
+    {
+      for (unsigned int h = 0; h < height; ++h)
+      {
+        uint16_t* pixel = reinterpret_cast<uint16_t*>(GetRow(h));
+        
+        for (unsigned int w = 0; w < GetWidth(); ++w, ++pixel)
+        {
+          // memcpy() is necessary to avoid segmentation fault if the
+          // "pixel" pointer is not 16-bit aligned (which is the case
+          // if "offset" is an odd number). Check out issue #99:
+          // https://bitbucket.org/sjodogne/orthanc/issues/99
+          uint16_t v = htobe16(*pixel);
+          memcpy(pixel, &v, sizeof(v));
+        }
+      }
+    }
+  }
+
+  
+#if ORTHANC_SANDBOXED == 0
+  void PamReader::ReadFromFile(const std::string& filename)
+  {
+    SystemToolbox::ReadFile(content_, filename);
+    ParseContent();
+  }
+#endif
+  
+
+  void PamReader::ReadFromMemory(const std::string& buffer)
+  {
+    content_ = buffer;
+    ParseContent();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/PamReader.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ImageAccessor.h"
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+namespace Orthanc
+{
+  class PamReader : public ImageAccessor
+  {
+  private:
+    void ParseContent();
+    
+    std::string content_;
+
+  public:
+#if ORTHANC_SANDBOXED == 0
+    void ReadFromFile(const std::string& filename);
+#endif
+
+    void ReadFromMemory(const std::string& buffer);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/PamWriter.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,159 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "PamWriter.h"
+
+#include "../Endianness.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#include <boost/lexical_cast.hpp>
+
+
+namespace Orthanc
+{
+  static void GetPixelFormatInfo(const PixelFormat& format,
+                                 unsigned int& maxValue,
+                                 unsigned int& channelCount,
+                                 unsigned int& bytesPerChannel,
+                                 std::string& tupleType)
+  {
+    switch (format)
+    {
+      case PixelFormat_Grayscale8:
+        maxValue = 255;
+        channelCount = 1;
+        bytesPerChannel = 1;
+        tupleType = "GRAYSCALE";
+        break;
+          
+      case PixelFormat_Grayscale16:
+        maxValue = 65535;
+        channelCount = 1;
+        bytesPerChannel = 2;
+        tupleType = "GRAYSCALE";
+        break;
+
+      case PixelFormat_RGB24:
+        maxValue = 255;
+        channelCount = 3;
+        bytesPerChannel = 1;
+        tupleType = "RGB";
+        break;
+
+      case PixelFormat_RGB48:
+        maxValue = 255;
+        channelCount = 3;
+        bytesPerChannel = 2;
+        tupleType = "RGB";
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+      
+  void PamWriter::WriteToMemoryInternal(std::string& target,
+                                        unsigned int width,
+                                        unsigned int height,
+                                        unsigned int sourcePitch,
+                                        PixelFormat format,
+                                        const void* buffer)
+  {
+    unsigned int maxValue, channelCount, bytesPerChannel;
+    std::string tupleType;
+    GetPixelFormatInfo(format, maxValue, channelCount, bytesPerChannel, tupleType);
+
+    target = (std::string("P7") +
+              std::string("\nWIDTH ")  + boost::lexical_cast<std::string>(width) + 
+              std::string("\nHEIGHT ") + boost::lexical_cast<std::string>(height) + 
+              std::string("\nDEPTH ")  + boost::lexical_cast<std::string>(channelCount) + 
+              std::string("\nMAXVAL ") + boost::lexical_cast<std::string>(maxValue) + 
+              std::string("\nTUPLTYPE ") + tupleType + 
+              std::string("\nENDHDR\n"));
+
+    if (bytesPerChannel != 1 &&
+        bytesPerChannel != 2)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    size_t targetPitch = channelCount * bytesPerChannel * width;
+    size_t offset = target.size();
+
+    target.resize(offset + targetPitch * height);
+
+    assert(target.size() != 0);
+
+    if (Toolbox::DetectEndianness() == Endianness_Little &&
+        bytesPerChannel == 2)
+    {
+      // Byte swapping
+      for (unsigned int h = 0; h < height; ++h)
+      {
+        const uint16_t* p = reinterpret_cast<const uint16_t*>
+          (reinterpret_cast<const uint8_t*>(buffer) + h * sourcePitch);
+        uint16_t* q = reinterpret_cast<uint16_t*>
+          (reinterpret_cast<uint8_t*>(&target[offset]) + h * targetPitch);
+        
+        for (unsigned int w = 0; w < width * channelCount; ++w)
+        {
+          // memcpy() is necessary to avoid segmentation fault if the
+          // "pixel" pointer is not 16-bit aligned (which is the case
+          // if "offset" is an odd number). Check out issue #99:
+          // https://bitbucket.org/sjodogne/orthanc/issues/99
+          uint16_t v = htobe16(*p);
+          memcpy(q, &v, sizeof(uint16_t));
+
+          p++;
+          q++;
+        }
+      }
+    }
+    else
+    {
+      // Either "bytesPerChannel == 1" (and endianness is not
+      // relevant), or we run on a big endian architecture (and no
+      // byte swapping is necessary, as PAM uses big endian)
+      
+      for (unsigned int h = 0; h < height; ++h)
+      {
+        const void* p = reinterpret_cast<const uint8_t*>(buffer) + h * sourcePitch;
+        void* q = reinterpret_cast<uint8_t*>(&target[offset]) + h * targetPitch;
+        memcpy(q, p, targetPitch);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/PamWriter.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IImageWriter.h"
+
+namespace Orthanc
+{
+  // https://en.wikipedia.org/wiki/Netpbm#PAM_graphics_format
+  class PamWriter : public IImageWriter
+  {
+  protected:
+    virtual void WriteToMemoryInternal(std::string& target,
+                                       unsigned int width,
+                                       unsigned int height,
+                                       unsigned int pitch,
+                                       PixelFormat format,
+                                       const void* buffer);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/PixelTraits.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,395 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Enumerations.h"
+#include "../OrthancException.h"
+
+#include <limits>
+
+namespace Orthanc
+{
+  template <PixelFormat format,
+            typename _PixelType>
+  struct IntegerPixelTraits
+  {
+    typedef _PixelType  PixelType;
+
+    ORTHANC_FORCE_INLINE
+    static PixelFormat GetPixelFormat()
+    {
+      return format;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static PixelType IntegerToPixel(int64_t value)
+    {
+      if (value < static_cast<int64_t>(std::numeric_limits<PixelType>::min()) ||
+          value > static_cast<int64_t>(std::numeric_limits<PixelType>::max()))
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        return static_cast<PixelType>(value);
+      }
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void SetZero(PixelType& target)
+    {
+      target = 0;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void SetMinValue(PixelType& target)
+    {
+      target = std::numeric_limits<PixelType>::min();
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void SetMaxValue(PixelType& target)
+    {
+      target = std::numeric_limits<PixelType>::max();
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void Copy(PixelType& target,
+                     const PixelType& source)
+    {
+      target = source;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static float PixelToFloat(const PixelType& source)
+    {
+      return static_cast<float>(source);
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void FloatToPixel(PixelType& target,
+                             float value)
+    {
+      if (value < static_cast<float>(std::numeric_limits<PixelType>::min()))
+      {
+        target = std::numeric_limits<PixelType>::min();
+      }
+      else if (value > static_cast<float>(std::numeric_limits<PixelType>::max()))
+      {
+        target = std::numeric_limits<PixelType>::max();
+      }
+      else
+      {
+        target = static_cast<PixelType>(value);
+      }
+    }
+
+    ORTHANC_FORCE_INLINE
+    static bool IsEqual(const PixelType& a,
+                        const PixelType& b)
+    {
+      return a == b;
+    }
+  };
+
+
+  template <PixelFormat Format>
+  struct PixelTraits;
+
+
+  template <>
+  struct PixelTraits<PixelFormat_Grayscale8> :
+    public IntegerPixelTraits<PixelFormat_Grayscale8, uint8_t>
+  {
+  };
+
+  
+  template <>
+  struct PixelTraits<PixelFormat_Grayscale16> :
+    public IntegerPixelTraits<PixelFormat_Grayscale16, uint16_t>
+  {
+  };
+
+  
+  template <>
+  struct PixelTraits<PixelFormat_SignedGrayscale16> :
+    public IntegerPixelTraits<PixelFormat_SignedGrayscale16, int16_t>
+  {
+  };
+
+
+  template <>
+  struct PixelTraits<PixelFormat_Grayscale32> :
+    public IntegerPixelTraits<PixelFormat_Grayscale32, uint32_t>
+  {
+  };
+
+
+  template <>
+  struct PixelTraits<PixelFormat_Grayscale64> :
+    public IntegerPixelTraits<PixelFormat_Grayscale64, uint64_t>
+  {
+  };
+
+
+  template <>
+  struct PixelTraits<PixelFormat_RGB24>
+  {
+    struct PixelType
+    {
+      uint8_t  red_;
+      uint8_t  green_;
+      uint8_t  blue_;
+    };
+
+    ORTHANC_FORCE_INLINE
+    static PixelFormat GetPixelFormat()
+    {
+      return PixelFormat_RGB24;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void SetZero(PixelType& target)
+    {
+      target.red_ = 0;
+      target.green_ = 0;
+      target.blue_ = 0;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void Copy(PixelType& target,
+                     const PixelType& source)
+    {
+      target.red_ = source.red_;
+      target.green_ = source.green_;
+      target.blue_ = source.blue_;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static bool IsEqual(const PixelType& a,
+                        const PixelType& b)
+    {
+      return (a.red_ == b.red_ &&
+              a.green_ == b.green_ &&
+              a.blue_ == b.blue_);
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void FloatToPixel(PixelType& target,
+                             float value)
+    {
+      uint8_t v;
+      PixelTraits<PixelFormat_Grayscale8>::FloatToPixel(v, value);
+
+      target.red_ = v;
+      target.green_ = v;
+      target.blue_ = v;
+    }
+  };
+
+
+  template <>
+  struct PixelTraits<PixelFormat_BGRA32>
+  {
+    struct PixelType
+    {
+      uint8_t  blue_;
+      uint8_t  green_;
+      uint8_t  red_;
+      uint8_t  alpha_;
+    };
+
+    ORTHANC_FORCE_INLINE
+    static PixelFormat GetPixelFormat()
+    {
+      return PixelFormat_BGRA32;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void SetZero(PixelType& target)
+    {
+      target.blue_ = 0;
+      target.green_ = 0;
+      target.red_ = 0;
+      target.alpha_ = 0;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void Copy(PixelType& target,
+                     const PixelType& source)
+    {
+      target.blue_ = source.blue_;
+      target.green_ = source.green_;
+      target.red_ = source.red_;
+      target.alpha_ = source.alpha_;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static bool IsEqual(const PixelType& a,
+                        const PixelType& b)
+    {
+      return (a.blue_ == b.blue_ &&
+              a.green_ == b.green_ &&
+              a.red_ == b.red_ &&
+              a.alpha_ == b.alpha_);
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void FloatToPixel(PixelType& target,
+                             float value)
+    {
+      uint8_t v;
+      PixelTraits<PixelFormat_Grayscale8>::FloatToPixel(v, value);
+
+      target.blue_ = v;
+      target.green_ = v;
+      target.red_ = v;
+      target.alpha_ = 255;      
+    }
+  };
+
+
+  template <>
+  struct PixelTraits<PixelFormat_RGBA32>
+  {
+    struct PixelType
+    {
+      uint8_t  red_;
+      uint8_t  green_;
+      uint8_t  blue_;
+      uint8_t  alpha_;
+    };
+
+    ORTHANC_FORCE_INLINE
+    static PixelFormat GetPixelFormat()
+    {
+      return PixelFormat_RGBA32;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void SetZero(PixelType& target)
+    {
+      target.red_ = 0;
+      target.green_ = 0;
+      target.blue_ = 0;
+      target.alpha_ = 0;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void Copy(PixelType& target,
+                     const PixelType& source)
+    {
+      target.red_ = source.red_;
+      target.green_ = source.green_;
+      target.blue_ = source.blue_;
+      target.alpha_ = source.alpha_;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static bool IsEqual(const PixelType& a,
+                        const PixelType& b)
+    {
+      return (a.red_ == b.red_ &&
+              a.green_ == b.green_ &&
+              a.blue_ == b.blue_ &&
+              a.alpha_ == b.alpha_);
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void FloatToPixel(PixelType& target,
+                             float value)
+    {
+      uint8_t v;
+      PixelTraits<PixelFormat_Grayscale8>::FloatToPixel(v, value);
+
+      target.red_ = v;
+      target.green_ = v;
+      target.blue_ = v;
+      target.alpha_ = 255;      
+    }
+  };
+
+
+  template <>
+  struct PixelTraits<PixelFormat_Float32>
+  {
+    typedef float  PixelType;
+
+    ORTHANC_FORCE_INLINE
+    static PixelFormat GetPixelFormat()
+    {
+      return PixelFormat_Float32;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void SetZero(PixelType& target)
+    {
+      target = 0.0f;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void Copy(PixelType& target,
+                     const PixelType& source)
+    {
+      target = source;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static bool IsEqual(const PixelType& a,
+                        const PixelType& b)
+    {
+      float tmp = (a - b);
+
+      if (tmp < 0)
+      {
+        tmp = -tmp;
+      }
+
+      return tmp <= std::numeric_limits<float>::epsilon();
+    }
+    
+    ORTHANC_FORCE_INLINE
+    static void FloatToPixel(PixelType& target,
+                             float value)
+    {
+      target = value;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static float PixelToFloat(const PixelType& source)
+    {
+      return source;
+    }
+  };
+}
--- a/Core/Images/PngReader.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/PngReader.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -36,11 +37,16 @@
 #include "../OrthancException.h"
 #include "../Toolbox.h"
 
+#if ORTHANC_SANDBOXED == 0
+#  include "../SystemToolbox.h"
+#endif
+
 #include <png.h>
 #include <string.h>  // For memcpy()
 
 namespace Orthanc
 {
+#if ORTHANC_SANDBOXED == 0
   namespace 
   {
     struct FileRabi
@@ -49,7 +55,7 @@
 
       FileRabi(const char* filename)
       {
-        fp_ = fopen(filename, "rb");
+        fp_ = SystemToolbox::OpenFile(filename, FileMode_ReadBinary);
         if (!fp_)
         {
           throw OrthancException(ErrorCode_InexistentFile);
@@ -59,10 +65,13 @@
       ~FileRabi()
       {
         if (fp_)
+        {
           fclose(fp_);
+        }
       }
     };
   }
+#endif
 
 
   struct PngReader::PngRabi
@@ -204,9 +213,11 @@
     AssignWritable(format, width, height, pitch, &data_[0]);
   }
 
-  void PngReader::ReadFromFile(const char* filename)
+
+#if ORTHANC_SANDBOXED == 0
+  void PngReader::ReadFromFile(const std::string& filename)
   {
-    FileRabi f(filename);
+    FileRabi f(filename.c_str());
 
     char header[8];
     if (fread(header, 1, 8, f.fp_) != 8)
@@ -228,6 +239,7 @@
 
     Read(rabi);
   }
+#endif
 
 
   namespace
--- a/Core/Images/PngReader.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/PngReader.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,6 +33,14 @@
 
 #pragma once
 
+#if !defined(ORTHANC_ENABLE_PNG)
+#  error The macro ORTHANC_ENABLE_PNG must be defined
+#endif
+
+#if ORTHANC_ENABLE_PNG != 1
+#  error PNG support must be enabled to include this file
+#endif
+
 #include "ImageAccessor.h"
 
 #include "../Enumerations.h"
@@ -40,6 +49,10 @@
 #include <stdint.h>
 #include <boost/shared_ptr.hpp>
 
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
 namespace Orthanc
 {
   class PngReader : public ImageAccessor
@@ -56,12 +69,9 @@
   public:
     PngReader();
 
-    void ReadFromFile(const char* filename);
-
-    void ReadFromFile(const std::string& filename)
-    {
-      ReadFromFile(filename.c_str());
-    }
+#if ORTHANC_SANDBOXED == 0
+    void ReadFromFile(const std::string& filename);
+#endif
 
     void ReadFromMemory(const void* buffer,
                         size_t size);
--- a/Core/Images/PngWriter.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/PngWriter.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -40,6 +41,10 @@
 #include "../ChunkedBuffer.h"
 #include "../Toolbox.h"
 
+#if ORTHANC_SANDBOXED == 0
+#  include "../SystemToolbox.h"
+#endif
+
 
 // http://www.libpng.org/pub/png/libpng-1.2.5-manual.html#section-4
 // http://zarb.org/~gc/html/libpng.html
@@ -202,16 +207,17 @@
   }
 
 
-  void PngWriter::WriteToFile(const char* filename,
-                              unsigned int width,
-                              unsigned int height,
-                              unsigned int pitch,
-                              PixelFormat format,
-                              const void* buffer)
+#if ORTHANC_SANDBOXED == 0
+  void PngWriter::WriteToFileInternal(const std::string& filename,
+                                      unsigned int width,
+                                      unsigned int height,
+                                      unsigned int pitch,
+                                      PixelFormat format,
+                                      const void* buffer)
   {
     Prepare(width, height, pitch, format, buffer);
 
-    FILE* fp = fopen(filename, "wb");
+    FILE* fp = SystemToolbox::OpenFile(filename, FileMode_WriteBinary);
     if (!fp)
     {
       throw OrthancException(ErrorCode_CannotWriteFile);
@@ -229,8 +235,7 @@
 
     fclose(fp);
   }
-
-
+#endif
 
 
   static void MemoryCallback(png_structp png_ptr, 
@@ -243,12 +248,12 @@
 
 
 
-  void PngWriter::WriteToMemory(std::string& png,
-                                unsigned int width,
-                                unsigned int height,
-                                unsigned int pitch,
-                                PixelFormat format,
-                                const void* buffer)
+  void PngWriter::WriteToMemoryInternal(std::string& png,
+                                        unsigned int width,
+                                        unsigned int height,
+                                        unsigned int pitch,
+                                        PixelFormat format,
+                                        const void* buffer)
   {
     ChunkedBuffer chunks;
 
--- a/Core/Images/PngWriter.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Images/PngWriter.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,15 +33,39 @@
 
 #pragma once
 
-#include "ImageAccessor.h"
+#if !defined(ORTHANC_ENABLE_PNG)
+#  error The macro ORTHANC_ENABLE_PNG must be defined
+#endif
+
+#if ORTHANC_ENABLE_PNG != 1
+#  error PNG support must be enabled to include this file
+#endif
+
+#include "IImageWriter.h"
 
 #include <boost/shared_ptr.hpp>
-#include <string>
 
 namespace Orthanc
 {
-  class PngWriter
+  class PngWriter : public IImageWriter
   {
+  protected:
+#if ORTHANC_SANDBOXED == 0
+    virtual void WriteToFileInternal(const std::string& filename,
+                                     unsigned int width,
+                                     unsigned int height,
+                                     unsigned int pitch,
+                                     PixelFormat format,
+                                     const void* buffer);
+#endif
+
+    virtual void WriteToMemoryInternal(std::string& png,
+                                       unsigned int width,
+                                       unsigned int height,
+                                       unsigned int pitch,
+                                       PixelFormat format,
+                                       const void* buffer);
+
   private:
     struct PImpl;
     boost::shared_ptr<PImpl> pimpl_;
@@ -60,33 +85,5 @@
     PngWriter();
 
     ~PngWriter();
-
-    void WriteToFile(const char* filename,
-                     unsigned int width,
-                     unsigned int height,
-                     unsigned int pitch,
-                     PixelFormat format,
-                     const void* buffer);
-
-    void WriteToMemory(std::string& png,
-                       unsigned int width,
-                       unsigned int height,
-                       unsigned int pitch,
-                       PixelFormat format,
-                       const void* buffer);
-
-    void WriteToFile(const char* filename,
-                     const ImageAccessor& accessor)
-    {
-      WriteToFile(filename, accessor.GetWidth(), accessor.GetHeight(),
-                  accessor.GetPitch(), accessor.GetFormat(), accessor.GetConstBuffer());
-    }
-
-    void WriteToMemory(std::string& png,
-                       const ImageAccessor& accessor)
-    {
-      WriteToMemory(png, accessor.GetWidth(), accessor.GetHeight(),
-                    accessor.GetPitch(), accessor.GetFormat(), accessor.GetConstBuffer());
-    }
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/GenericJobUnserializer.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,99 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "GenericJobUnserializer.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+
+#include "Operations/LogJobOperation.h"
+#include "Operations/NullOperationValue.h"
+#include "Operations/SequenceOfOperationsJob.h"
+#include "Operations/StringOperationValue.h"
+
+namespace Orthanc
+{
+  IJob* GenericJobUnserializer::UnserializeJob(const Json::Value& source)
+  {
+    const std::string type = SerializationToolbox::ReadString(source, "Type");
+
+    if (type == "SequenceOfOperations")
+    {
+      return new SequenceOfOperationsJob(*this, source);
+    }
+    else
+    {
+      LOG(ERROR) << "Cannot unserialize job of type: " << type;
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  IJobOperation* GenericJobUnserializer::UnserializeOperation(const Json::Value& source)
+  {
+    const std::string type = SerializationToolbox::ReadString(source, "Type");
+
+    if (type == "Log")
+    {
+      return new LogJobOperation;
+    }
+    else
+    {
+      LOG(ERROR) << "Cannot unserialize operation of type: " << type;
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  JobOperationValue* GenericJobUnserializer::UnserializeValue(const Json::Value& source)
+  {
+    const std::string type = SerializationToolbox::ReadString(source, "Type");
+
+    if (type == "String")
+    {
+      return new StringOperationValue(SerializationToolbox::ReadString(source, "Content"));
+    }
+    else if (type == "Null")
+    {
+      return new NullOperationValue;
+    }
+    else
+    {
+      LOG(ERROR) << "Cannot unserialize value of type: " << type;
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/GenericJobUnserializer.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,49 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IJobUnserializer.h"
+
+namespace Orthanc
+{
+  class GenericJobUnserializer : public IJobUnserializer
+  {
+  public:
+    virtual IJob* UnserializeJob(const Json::Value& value);
+
+    virtual IJobOperation* UnserializeOperation(const Json::Value& value);
+
+    virtual JobOperationValue* UnserializeValue(const Json::Value& value);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/IJob.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,69 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "JobStepResult.h"
+
+#include <boost/noncopyable.hpp>
+#include <json/value.h>
+
+namespace Orthanc
+{
+  class IJob : public boost::noncopyable
+  {
+  public:
+    virtual ~IJob()
+    {
+    }
+
+    // Method called once the job enters the jobs engine
+    virtual void Start() = 0;
+    
+    virtual JobStepResult Step() = 0;
+
+    // Method called once the job is resubmitted after a failure
+    virtual void Reset() = 0;
+
+    // For pausing/canceling/ending jobs: This method must release allocated resources
+    virtual void Stop(JobStopReason reason) = 0;
+
+    virtual float GetProgress() = 0;
+
+    virtual void GetJobType(std::string& target) = 0;
+    
+    virtual void GetPublicContent(Json::Value& value) = 0;
+
+    virtual bool Serialize(Json::Value& value) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/IJobUnserializer.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IJob.h"
+#include "Operations/JobOperationValue.h"
+#include "Operations/IJobOperation.h"
+
+#include <vector>
+
+namespace Orthanc
+{
+  class IJobUnserializer : public boost::noncopyable
+  {
+  public:
+    virtual ~IJobUnserializer()
+    {
+    }
+
+    virtual IJob* UnserializeJob(const Json::Value& value) = 0;
+
+    virtual IJobOperation* UnserializeOperation(const Json::Value& value) = 0;
+
+    virtual JobOperationValue* UnserializeValue(const Json::Value& value) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobInfo.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,148 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "JobInfo.h"
+
+#include "../OrthancException.h"
+
+// This "include" is mandatory for Release builds using Linux Standard Base
+#include <boost/math/special_functions/round.hpp>
+
+namespace Orthanc
+{
+  JobInfo::JobInfo(const std::string& id,
+                   int priority,
+                   JobState state,
+                   const JobStatus& status,
+                   const boost::posix_time::ptime& creationTime,
+                   const boost::posix_time::ptime& lastStateChangeTime,
+                   const boost::posix_time::time_duration& runtime) :
+    id_(id),
+    priority_(priority),
+    state_(state),
+    timestamp_(boost::posix_time::microsec_clock::universal_time()),
+    creationTime_(creationTime),
+    lastStateChangeTime_(lastStateChangeTime),
+    runtime_(runtime),
+    hasEta_(false),
+    status_(status)
+  {
+    if (state_ == JobState_Running)
+    {
+      float ms = static_cast<float>(runtime_.total_milliseconds());
+
+      if (status_.GetProgress() > 0.01f &&
+          ms > 0.01f)
+      {
+        float ratio = static_cast<float>(1.0 - status_.GetProgress());
+        long long remaining = boost::math::llround(ratio * ms);
+        eta_ = timestamp_ + boost::posix_time::milliseconds(remaining);
+        hasEta_ = true;
+      }
+    }
+  }
+
+
+  JobInfo::JobInfo() :
+    priority_(0),
+    state_(JobState_Failure),
+    timestamp_(boost::posix_time::microsec_clock::universal_time()),
+    creationTime_(timestamp_),
+    lastStateChangeTime_(timestamp_),
+    runtime_(boost::posix_time::milliseconds(0)),
+    hasEta_(false)
+  {
+  }
+
+
+  bool JobInfo::HasCompletionTime() const
+  {
+    return (state_ == JobState_Success ||
+            state_ == JobState_Failure);
+  }
+
+
+  const boost::posix_time::ptime& JobInfo::GetEstimatedTimeOfArrival() const
+  {
+    if (hasEta_)
+    {
+      return eta_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const boost::posix_time::ptime& JobInfo::GetCompletionTime() const
+  {
+    if (HasCompletionTime())
+    {
+      return lastStateChangeTime_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void JobInfo::Format(Json::Value& target) const
+  {
+    target = Json::objectValue;
+    target["ID"] = id_;
+    target["Priority"] = priority_;
+    target["ErrorCode"] = static_cast<int>(status_.GetErrorCode());
+    target["ErrorDescription"] = EnumerationToString(status_.GetErrorCode());
+    target["State"] = EnumerationToString(state_);
+    target["Timestamp"] = boost::posix_time::to_iso_string(timestamp_);
+    target["CreationTime"] = boost::posix_time::to_iso_string(creationTime_);
+    target["EffectiveRuntime"] = static_cast<double>(runtime_.total_milliseconds()) / 1000.0;
+    target["Progress"] = boost::math::iround(status_.GetProgress() * 100.0f);
+
+    target["Type"] = status_.GetJobType();
+    target["Content"] = status_.GetPublicContent();
+
+    if (HasEstimatedTimeOfArrival())
+    {
+      target["EstimatedTimeOfArrival"] = boost::posix_time::to_iso_string(GetEstimatedTimeOfArrival());
+    }
+
+    if (HasCompletionTime())
+    {
+      target["CompletionTime"] = boost::posix_time::to_iso_string(GetCompletionTime());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobInfo.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "JobStatus.h"
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+namespace Orthanc
+{
+  class JobInfo
+  {
+  private:
+    std::string                       id_;
+    int                               priority_;
+    JobState                          state_;
+    boost::posix_time::ptime          timestamp_;
+    boost::posix_time::ptime          creationTime_;
+    boost::posix_time::ptime          lastStateChangeTime_;
+    boost::posix_time::time_duration  runtime_;
+    bool                              hasEta_;
+    boost::posix_time::ptime          eta_;
+    JobStatus                         status_;
+
+  public:
+    JobInfo(const std::string& id,
+            int priority,
+            JobState state,
+            const JobStatus& status,
+            const boost::posix_time::ptime& creationTime,
+            const boost::posix_time::ptime& lastStateChangeTime,
+            const boost::posix_time::time_duration& runtime);
+
+    JobInfo();
+
+    const std::string& GetIdentifier() const
+    {
+      return id_;
+    }
+
+    int GetPriority() const
+    {
+      return priority_;
+    }
+
+    JobState GetState() const
+    {
+      return state_;
+    }
+
+    const boost::posix_time::ptime& GetInfoTime() const
+    {
+      return timestamp_;
+    }
+
+    const boost::posix_time::ptime& GetCreationTime() const
+    {
+      return creationTime_;
+    }
+
+    const boost::posix_time::time_duration& GetRuntime() const
+    {
+      return runtime_;
+    }
+
+    bool HasEstimatedTimeOfArrival() const
+    {
+      return hasEta_;
+    }
+
+    bool HasCompletionTime() const;
+
+    const boost::posix_time::ptime& GetEstimatedTimeOfArrival() const;
+
+    const boost::posix_time::ptime& GetCompletionTime() const;
+
+    const JobStatus& GetStatus() const
+    {
+      return status_;
+    }
+
+    JobStatus& GetStatus()
+    {
+      return status_;
+    }
+
+    void Format(Json::Value& target) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobStatus.cpp	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "JobStatus.h"
+
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  JobStatus::JobStatus() :
+    errorCode_(ErrorCode_InternalError),
+    progress_(0),
+    jobType_("Invalid"),
+    publicContent_(Json::objectValue),
+    hasSerialized_(false)
+  {
+  }
+
+  
+  JobStatus::JobStatus(ErrorCode code,
+                       IJob& job) :
+    errorCode_(code),
+    progress_(job.GetProgress()),
+    publicContent_(Json::objectValue)
+  {
+    if (progress_ < 0)
+    {
+      progress_ = 0;
+    }
+      
+    if (progress_ > 1)
+    {
+      progress_ = 1;
+    }
+
+    job.GetJobType(jobType_);
+    job.GetPublicContent(publicContent_);
+
+    hasSerialized_ = job.Serialize(serialized_);
+  }
+
+
+  const Json::Value& JobStatus::GetSerialized() const
+  {
+    if (!hasSerialized_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return serialized_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobStatus.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IJob.h"
+
+namespace Orthanc
+{
+  class JobStatus
+  {
+  private:
+    ErrorCode      errorCode_;
+    float          progress_;
+    std::string    jobType_;
+    Json::Value    publicContent_;
+    Json::Value    serialized_;
+    bool           hasSerialized_;
+
+  public:
+    JobStatus();
+
+    JobStatus(ErrorCode code,
+              IJob& job);
+
+    ErrorCode GetErrorCode() const
+    {
+      return errorCode_;
+    }
+
+    void SetErrorCode(ErrorCode error)
+    {
+      errorCode_ = error;
+    }
+
+    float GetProgress() const
+    {
+      return progress_;
+    }
+
+    const std::string& GetJobType() const
+    {
+      return jobType_;
+    }
+
+    const Json::Value& GetPublicContent() const
+    {
+      return publicContent_;
+    }
+
+    const Json::Value& GetSerialized() const;
+
+    bool HasSerialized() const
+    {
+      return hasSerialized_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobStepResult.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,81 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "JobStepResult.h"
+
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  JobStepResult JobStepResult::Retry(unsigned int timeout)
+  {
+    JobStepResult result(JobStepCode_Retry);
+    result.timeout_ = timeout;
+    return result;
+  }
+
+
+  JobStepResult JobStepResult::Failure(const ErrorCode& error)
+  {
+    JobStepResult result(JobStepCode_Failure);
+    result.error_ = error;
+    return result;
+  }
+
+
+  unsigned int JobStepResult::GetRetryTimeout() const
+  {
+    if (code_ == JobStepCode_Retry)
+    {
+      return timeout_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  ErrorCode JobStepResult::GetFailureCode() const
+  {
+    if (code_ == JobStepCode_Failure)
+    {
+      return error_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobStepResult.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Enumerations.h"
+
+namespace Orthanc
+{
+  class JobStepResult
+  {
+  private:
+    JobStepCode   code_;
+    unsigned int  timeout_;
+    ErrorCode     error_;
+    
+    explicit JobStepResult(JobStepCode code) :
+      code_(code),
+      timeout_(0),
+      error_(ErrorCode_Success)
+    {
+    }
+
+  public:
+    explicit JobStepResult() :
+      code_(JobStepCode_Failure),
+      timeout_(0),
+      error_(ErrorCode_InternalError)
+    {
+    }
+
+    static JobStepResult Success()
+    {
+      return JobStepResult(JobStepCode_Success);
+    }
+
+    static JobStepResult Continue()
+    {
+      return JobStepResult(JobStepCode_Continue);
+    }
+
+    static JobStepResult Retry(unsigned int timeout);
+
+    static JobStepResult Failure(const ErrorCode& error);
+
+    JobStepCode GetCode() const
+    {
+      return code_;
+    }
+
+    unsigned int GetRetryTimeout() const;
+
+    ErrorCode GetFailureCode() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobsEngine.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,324 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "JobsEngine.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+#include <json/reader.h>
+
+namespace Orthanc
+{
+  bool JobsEngine::IsRunning()
+  {
+    boost::mutex::scoped_lock lock(stateMutex_);
+    return (state_ == State_Running);
+  }
+  
+  
+  bool JobsEngine::ExecuteStep(JobsRegistry::RunningJob& running,
+                               size_t workerIndex)
+  {
+    assert(running.IsValid());
+
+    if (running.IsPauseScheduled())
+    {
+      running.GetJob().Stop(JobStopReason_Paused);
+      running.MarkPause();
+      return false;
+    }
+
+    if (running.IsCancelScheduled())
+    {
+      running.GetJob().Stop(JobStopReason_Canceled);
+      running.MarkCanceled();
+      return false;
+    }
+
+    JobStepResult result;
+
+    try
+    {
+      result = running.GetJob().Step();
+    }
+    catch (OrthancException& e)
+    {
+      result = JobStepResult::Failure(e.GetErrorCode());
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      result = JobStepResult::Failure(ErrorCode_BadFileFormat);
+    }
+    catch (...)
+    {
+      result = JobStepResult::Failure(ErrorCode_InternalError);
+    }
+
+    switch (result.GetCode())
+    {
+      case JobStepCode_Success:
+        running.GetJob().Stop(JobStopReason_Success);
+        running.UpdateStatus(ErrorCode_Success);
+        running.MarkSuccess();
+        return false;
+
+      case JobStepCode_Failure:
+        running.GetJob().Stop(JobStopReason_Failure);
+        running.UpdateStatus(result.GetFailureCode());
+        running.MarkFailure();
+        return false;
+
+      case JobStepCode_Retry:
+        running.GetJob().Stop(JobStopReason_Retry);
+        running.UpdateStatus(ErrorCode_Success);
+        running.MarkRetry(result.GetRetryTimeout());
+        return false;
+
+      case JobStepCode_Continue:
+        running.UpdateStatus(ErrorCode_Success);
+        return true;
+            
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+    
+  void JobsEngine::RetryHandler(JobsEngine* engine)
+  {
+    assert(engine != NULL);
+
+    while (engine->IsRunning())
+    {
+      boost::this_thread::sleep(boost::posix_time::milliseconds(engine->threadSleep_));
+      engine->GetRegistry().ScheduleRetries();
+    }
+  }
+
+    
+  void JobsEngine::Worker(JobsEngine* engine,
+                          size_t workerIndex)
+  {
+    assert(engine != NULL);
+
+    LOG(INFO) << "Worker thread " << workerIndex << " has started";
+
+    while (engine->IsRunning())
+    {
+      JobsRegistry::RunningJob running(engine->GetRegistry(), engine->threadSleep_);
+
+      if (running.IsValid())
+      {
+        LOG(INFO) << "Executing job with priority " << running.GetPriority()
+                  << " in worker thread " << workerIndex << ": " << running.GetId();
+
+        while (engine->IsRunning())
+        {
+          if (!engine->ExecuteStep(running, workerIndex))
+          {
+            break;
+          }
+        }
+      }
+    }      
+  }
+
+
+  JobsEngine::JobsEngine() :
+    state_(State_Setup),
+    registry_(new JobsRegistry),
+    threadSleep_(200),
+    workers_(1)
+  {
+  }
+
+    
+  JobsEngine::~JobsEngine()
+  {
+    if (state_ != State_Setup &&
+        state_ != State_Done)
+    {
+      LOG(ERROR) << "INTERNAL ERROR: JobsEngine::Stop() should be invoked manually to avoid mess in the destruction order!";
+      Stop();
+    }
+  }
+
+ 
+  JobsRegistry& JobsEngine::GetRegistry()
+  {
+    if (registry_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    return *registry_;
+  }
+  
+   
+  void JobsEngine::LoadRegistryFromJson(IJobUnserializer& unserializer,
+                                        const Json::Value& serialized)
+  {
+    boost::mutex::scoped_lock lock(stateMutex_);
+      
+    if (state_ != State_Setup)
+    {
+      // Can only be invoked before calling "Start()"
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    registry_.reset(new JobsRegistry(unserializer, serialized));
+  }
+
+
+  void JobsEngine::LoadRegistryFromString(IJobUnserializer& unserializer,
+                                          const std::string& serialized)
+  {
+    Json::Value value;
+    Json::Reader reader;
+    if (reader.parse(serialized, value))
+    {
+      LoadRegistryFromJson(unserializer, value);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  void JobsEngine::SetWorkersCount(size_t count)
+  {
+    boost::mutex::scoped_lock lock(stateMutex_);
+      
+    if (state_ != State_Setup)
+    {
+      // Can only be invoked before calling "Start()"
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    workers_.resize(count);
+  }
+
+
+  void JobsEngine::SetThreadSleep(unsigned int sleep)
+  {
+    boost::mutex::scoped_lock lock(stateMutex_);
+      
+    if (state_ != State_Setup)
+    {
+      // Can only be invoked before calling "Start()"
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    threadSleep_ = sleep;
+  }
+
+
+  void JobsEngine::Start()
+  {
+    boost::mutex::scoped_lock lock(stateMutex_);
+
+    if (state_ != State_Setup)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    retryHandler_ = boost::thread(RetryHandler, this);
+
+    if (workers_.size() == 0)
+    {
+      // Use all the available CPUs
+      size_t n = boost::thread::hardware_concurrency();
+      
+      if (n == 0)
+      {
+        n = 1;
+      }
+
+      workers_.resize(n);
+    }      
+
+    for (size_t i = 0; i < workers_.size(); i++)
+    {
+      assert(workers_[i] == NULL);
+      workers_[i] = new boost::thread(Worker, this, i);
+    }
+
+    state_ = State_Running;
+
+    LOG(WARNING) << "The jobs engine has started with " << workers_.size() << " threads";
+  }
+
+
+  void JobsEngine::Stop()
+  {
+    {
+      boost::mutex::scoped_lock lock(stateMutex_);
+
+      if (state_ != State_Running)
+      {
+        return;
+      }
+        
+      state_ = State_Stopping;
+    }
+
+    LOG(INFO) << "Stopping the jobs engine";
+      
+    if (retryHandler_.joinable())
+    {
+      retryHandler_.join();
+    }
+      
+    for (size_t i = 0; i < workers_.size(); i++)
+    {
+      assert(workers_[i] != NULL);
+
+      if (workers_[i]->joinable())
+      {
+        workers_[i]->join();
+      }
+
+      delete workers_[i];
+    }
+      
+    {
+      boost::mutex::scoped_lock lock(stateMutex_);
+      state_ = State_Done;
+    }
+
+    LOG(WARNING) << "The jobs engine has stopped";
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobsEngine.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,91 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "JobsRegistry.h"
+
+#include <boost/thread.hpp>
+
+namespace Orthanc
+{
+  class JobsEngine
+  {
+  private:
+    enum State
+    {
+      State_Setup,
+      State_Running,
+      State_Stopping,
+      State_Done
+    };
+
+    boost::mutex                 stateMutex_;
+    State                        state_;
+    std::auto_ptr<JobsRegistry>  registry_;
+    boost::thread                retryHandler_;
+    unsigned int                 threadSleep_;
+    std::vector<boost::thread*>  workers_;
+
+    bool IsRunning();
+    
+    bool ExecuteStep(JobsRegistry::RunningJob& running,
+                     size_t workerIndex);
+    
+    static void RetryHandler(JobsEngine* engine);
+
+    static void Worker(JobsEngine* engine,
+                       size_t workerIndex);
+
+  public:
+    JobsEngine();
+
+    ~JobsEngine();
+
+    JobsRegistry& GetRegistry();
+
+    void LoadRegistryFromJson(IJobUnserializer& unserializer,
+                              const Json::Value& serialized);
+
+    void LoadRegistryFromString(IJobUnserializer& unserializer,
+                                const std::string& serialized);
+
+    void SetWorkersCount(size_t count);
+
+    void SetThreadSleep(unsigned int sleep);
+
+    void Start();
+
+    void Stop();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobsRegistry.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,1335 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "JobsRegistry.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+#include "../SerializationToolbox.h"
+
+namespace Orthanc
+{
+  static const char* STATE = "State";
+  static const char* TYPE = "Type";
+  static const char* PRIORITY = "Priority";
+  static const char* JOB = "Job";
+  static const char* JOBS = "Jobs";
+  static const char* JOBS_REGISTRY = "JobsRegistry";
+  static const char* MAX_COMPLETED_JOBS = "MaxCompletedJobs";
+  static const char* CREATION_TIME = "CreationTime";
+  static const char* LAST_CHANGE_TIME = "LastChangeTime";
+  static const char* RUNTIME = "Runtime";
+  
+
+  class JobsRegistry::JobHandler : public boost::noncopyable
+  {   
+  private:
+    std::string                       id_;
+    JobState                          state_;
+    std::string                       jobType_;
+    std::auto_ptr<IJob>               job_;
+    int                               priority_;  // "+inf()" means highest priority
+    boost::posix_time::ptime          creationTime_;
+    boost::posix_time::ptime          lastStateChangeTime_;
+    boost::posix_time::time_duration  runtime_;
+    boost::posix_time::ptime          retryTime_;
+    bool                              pauseScheduled_;
+    bool                              cancelScheduled_;
+    JobStatus                         lastStatus_;
+
+    void Touch()
+    {
+      const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();
+
+      if (state_ == JobState_Running)
+      {
+        runtime_ += (now - lastStateChangeTime_);
+      }
+
+      lastStateChangeTime_ = now;
+    }
+
+    void SetStateInternal(JobState state) 
+    {
+      state_ = state;
+      pauseScheduled_ = false;
+      cancelScheduled_ = false;
+      Touch();
+    }
+
+  public:
+    JobHandler(IJob* job,
+               int priority) :
+      id_(Toolbox::GenerateUuid()),
+      state_(JobState_Pending),
+      job_(job),
+      priority_(priority),
+      creationTime_(boost::posix_time::microsec_clock::universal_time()),
+      lastStateChangeTime_(creationTime_),
+      runtime_(boost::posix_time::milliseconds(0)),
+      retryTime_(creationTime_),
+      pauseScheduled_(false),
+      cancelScheduled_(false)
+    {
+      if (job == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+
+      job->GetJobType(jobType_);
+      job->Start();
+
+      lastStatus_ = JobStatus(ErrorCode_Success, *job_);
+    }
+
+    const std::string& GetId() const
+    {
+      return id_;
+    }
+
+    IJob& GetJob() const
+    {
+      assert(job_.get() != NULL);
+      return *job_;
+    }
+
+    void SetPriority(int priority)
+    {
+      priority_ = priority;
+    }
+
+    int GetPriority() const
+    {
+      return priority_;
+    }
+
+    JobState GetState() const
+    {
+      return state_;
+    }
+
+    void SetState(JobState state) 
+    {
+      if (state == JobState_Retry)
+      {
+        // Use "SetRetryState()"
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        SetStateInternal(state);
+      }
+    }
+
+    void SetRetryState(unsigned int timeout)
+    {
+      if (state_ == JobState_Running)
+      {
+        SetStateInternal(JobState_Retry);
+        retryTime_ = (boost::posix_time::microsec_clock::universal_time() + 
+                      boost::posix_time::milliseconds(timeout));
+      }
+      else
+      {
+        // Only valid for running jobs
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    void SchedulePause()
+    {
+      if (state_ == JobState_Running)
+      {
+        pauseScheduled_ = true;
+      }
+      else
+      {
+        // Only valid for running jobs
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    void ScheduleCancel()
+    {
+      if (state_ == JobState_Running)
+      {
+        cancelScheduled_ = true;
+      }
+      else
+      {
+        // Only valid for running jobs
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    bool IsPauseScheduled()
+    {
+      return pauseScheduled_;
+    }
+
+    bool IsCancelScheduled()
+    {
+      return cancelScheduled_;
+    }
+
+    bool IsRetryReady(const boost::posix_time::ptime& now) const
+    {
+      if (state_ != JobState_Retry)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return retryTime_ <= now;
+      }
+    }
+
+    const boost::posix_time::ptime& GetCreationTime() const
+    {
+      return creationTime_;
+    }
+
+    const boost::posix_time::ptime& GetLastStateChangeTime() const
+    {
+      return lastStateChangeTime_;
+    }
+
+    void SetLastStateChangeTime(const boost::posix_time::ptime& time)
+    {
+      lastStateChangeTime_ = time;
+    }
+
+    const boost::posix_time::time_duration& GetRuntime() const
+    {
+      return runtime_;
+    }
+
+    const JobStatus& GetLastStatus() const
+    {
+      return lastStatus_;
+    }
+
+    void SetLastStatus(const JobStatus& status)
+    {
+      lastStatus_ = status;
+      Touch();
+    }
+
+    void SetLastErrorCode(ErrorCode code)
+    {
+      lastStatus_.SetErrorCode(code);
+    }
+
+    bool Serialize(Json::Value& target) const
+    {
+      target = Json::objectValue;
+
+      bool ok;
+
+      if (state_ == JobState_Running)
+      {
+        // WARNING: Cannot directly access the "job_" member, as long
+        // as a "RunningJob" instance is running. We do not use a
+        // mutex at the "JobHandler" level, as serialization would be
+        // blocked while a step in the job is running. Instead, we
+        // save a snapshot of the serialized job.
+
+        if (lastStatus_.HasSerialized())
+        {
+          target[JOB] = lastStatus_.GetSerialized();
+          ok = true;
+        }
+        else
+        {
+          ok = false;
+        }
+      }
+      else 
+      {
+        ok = job_->Serialize(target[JOB]);
+      }
+
+      if (ok)
+      {
+        target[STATE] = EnumerationToString(state_);
+        target[PRIORITY] = priority_;
+        target[CREATION_TIME] = boost::posix_time::to_iso_string(creationTime_);
+        target[LAST_CHANGE_TIME] = boost::posix_time::to_iso_string(lastStateChangeTime_);
+        target[RUNTIME] = static_cast<unsigned int>(runtime_.total_milliseconds());
+        return true;
+      }
+      else
+      {
+        LOG(INFO) << "Job backup is not supported for job of type: " << jobType_;
+        return false;
+      }
+    }
+
+    JobHandler(IJobUnserializer& unserializer,
+               const Json::Value& serialized,
+               const std::string& id) :
+      id_(id),
+      pauseScheduled_(false),
+      cancelScheduled_(false)
+    {
+      state_ = StringToJobState(SerializationToolbox::ReadString(serialized, STATE));
+      priority_ = SerializationToolbox::ReadInteger(serialized, PRIORITY);
+      creationTime_ = boost::posix_time::from_iso_string
+        (SerializationToolbox::ReadString(serialized, CREATION_TIME));
+      lastStateChangeTime_ = boost::posix_time::from_iso_string
+        (SerializationToolbox::ReadString(serialized, LAST_CHANGE_TIME));
+      runtime_ = boost::posix_time::milliseconds
+        (SerializationToolbox::ReadInteger(serialized, RUNTIME));
+
+      retryTime_ = creationTime_;
+
+      job_.reset(unserializer.UnserializeJob(serialized[JOB]));
+      job_->GetJobType(jobType_);
+      job_->Start();
+
+      lastStatus_ = JobStatus(ErrorCode_Success, *job_);
+    }
+  };
+
+
+  bool JobsRegistry::PriorityComparator::operator() (JobHandler*& a,
+                                                     JobHandler*& b) const
+  {
+    return a->GetPriority() < b->GetPriority();
+  }                       
+
+
+#if defined(NDEBUG)
+  void JobsRegistry::CheckInvariants() const
+  {
+  }
+  
+#else
+  bool JobsRegistry::IsPendingJob(const JobHandler& job) const
+  {
+    PendingJobs copy = pendingJobs_;
+    while (!copy.empty())
+    {
+      if (copy.top() == &job)
+      {
+        return true;
+      }
+
+      copy.pop();
+    }
+
+    return false;
+  }
+
+  bool JobsRegistry::IsCompletedJob(JobHandler& job) const
+  {
+    for (CompletedJobs::const_iterator it = completedJobs_.begin();
+         it != completedJobs_.end(); ++it)
+    {
+      if (*it == &job)
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  bool JobsRegistry::IsRetryJob(JobHandler& job) const
+  {
+    return retryJobs_.find(&job) != retryJobs_.end();
+  }
+
+  void JobsRegistry::CheckInvariants() const
+  {
+    {
+      PendingJobs copy = pendingJobs_;
+      while (!copy.empty())
+      {
+        assert(copy.top()->GetState() == JobState_Pending);
+        copy.pop();
+      }
+    }
+
+    assert(completedJobs_.size() <= maxCompletedJobs_);
+
+    for (CompletedJobs::const_iterator it = completedJobs_.begin();
+         it != completedJobs_.end(); ++it)
+    {
+      assert((*it)->GetState() == JobState_Success ||
+             (*it)->GetState() == JobState_Failure);
+    }
+
+    for (RetryJobs::const_iterator it = retryJobs_.begin();
+         it != retryJobs_.end(); ++it)
+    {
+      assert((*it)->GetState() == JobState_Retry);
+    }
+
+    for (JobsIndex::const_iterator it = jobsIndex_.begin();
+         it != jobsIndex_.end(); ++it)
+    {
+      JobHandler& job = *it->second;
+
+      assert(job.GetId() == it->first);
+
+      switch (job.GetState())
+      {
+        case JobState_Pending:
+          assert(!IsRetryJob(job) && IsPendingJob(job) && !IsCompletedJob(job));
+          break;
+            
+        case JobState_Success:
+        case JobState_Failure:
+          assert(!IsRetryJob(job) && !IsPendingJob(job) && IsCompletedJob(job));
+          break;
+            
+        case JobState_Retry:
+          assert(IsRetryJob(job) && !IsPendingJob(job) && !IsCompletedJob(job));
+          break;
+            
+        case JobState_Running:
+        case JobState_Paused:
+          assert(!IsRetryJob(job) && !IsPendingJob(job) && !IsCompletedJob(job));
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+#endif
+
+
+  void JobsRegistry::ForgetOldCompletedJobs()
+  {
+    if (maxCompletedJobs_ != 0)
+    {
+      while (completedJobs_.size() > maxCompletedJobs_)
+      {
+        assert(completedJobs_.front() != NULL);
+
+        std::string id = completedJobs_.front()->GetId();
+        assert(jobsIndex_.find(id) != jobsIndex_.end());
+
+        jobsIndex_.erase(id);
+        delete(completedJobs_.front());
+        completedJobs_.pop_front();
+      }
+    }
+  }
+
+
+  void JobsRegistry::SetCompletedJob(JobHandler& job,
+                                     bool success)
+  {
+    job.SetState(success ? JobState_Success : JobState_Failure);
+
+    completedJobs_.push_back(&job);
+    ForgetOldCompletedJobs();
+
+    someJobComplete_.notify_all();
+  }
+
+
+  void JobsRegistry::MarkRunningAsCompleted(JobHandler& job,
+                                            bool success)
+  {
+    LOG(INFO) << "Job has completed with " << (success ? "success" : "failure")
+              << ": " << job.GetId();
+
+    CheckInvariants();
+
+    assert(job.GetState() == JobState_Running);
+    SetCompletedJob(job, success);
+
+    if (observer_ != NULL)
+    {
+      if (success)
+      {
+        observer_->SignalJobSuccess(job.GetId());
+      }
+      else
+      {
+        observer_->SignalJobFailure(job.GetId());
+      }
+    }
+
+    CheckInvariants();
+  }
+
+
+  void JobsRegistry::MarkRunningAsRetry(JobHandler& job,
+                                        unsigned int timeout)
+  {
+    LOG(INFO) << "Job scheduled for retry in " << timeout << "ms: " << job.GetId();
+
+    CheckInvariants();
+
+    assert(job.GetState() == JobState_Running &&
+           retryJobs_.find(&job) == retryJobs_.end());
+
+    retryJobs_.insert(&job);
+    job.SetRetryState(timeout);
+
+    CheckInvariants();
+  }
+
+
+  void JobsRegistry::MarkRunningAsPaused(JobHandler& job)
+  {
+    LOG(INFO) << "Job paused: " << job.GetId();
+
+    CheckInvariants();
+    assert(job.GetState() == JobState_Running);
+
+    job.SetState(JobState_Paused);
+
+    CheckInvariants();
+  }
+
+
+  bool JobsRegistry::GetStateInternal(JobState& state,
+                                      const std::string& id)
+  {
+    CheckInvariants();
+
+    JobsIndex::const_iterator it = jobsIndex_.find(id);
+    if (it == jobsIndex_.end())
+    {
+      return false;
+    }
+    else
+    {
+      state = it->second->GetState();
+      return true;
+    }
+  }
+
+  
+  JobsRegistry::~JobsRegistry()
+  {
+    for (JobsIndex::iterator it = jobsIndex_.begin(); it != jobsIndex_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  void JobsRegistry::SetMaxCompletedJobs(size_t n)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    LOG(INFO) << "The size of the history of the jobs engine is set to: " << n << " job(s)";
+
+    maxCompletedJobs_ = n;
+    ForgetOldCompletedJobs();
+
+    CheckInvariants();
+  }
+
+
+  void JobsRegistry::ListJobs(std::set<std::string>& target)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    for (JobsIndex::const_iterator it = jobsIndex_.begin();
+         it != jobsIndex_.end(); ++it)
+    {
+      target.insert(it->first);
+    }
+  }
+
+
+  bool JobsRegistry::GetJobInfo(JobInfo& target,
+                                const std::string& id)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::const_iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      return false;
+    }
+    else
+    {
+      const JobHandler& handler = *found->second;
+      target = JobInfo(handler.GetId(),
+                       handler.GetPriority(),
+                       handler.GetState(),
+                       handler.GetLastStatus(),
+                       handler.GetCreationTime(),
+                       handler.GetLastStateChangeTime(),
+                       handler.GetRuntime());
+      return true;
+    }
+  }
+
+
+  void JobsRegistry::SubmitInternal(std::string& id,
+                                    JobHandler* handlerRaw,
+                                    bool keepLastChangeTime)
+  {
+    if (handlerRaw == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    
+    std::auto_ptr<JobHandler>  handler(handlerRaw);
+
+    boost::posix_time::ptime lastChangeTime = handler->GetLastStateChangeTime();
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      CheckInvariants();
+      
+      id = handler->GetId();
+      int priority = handler->GetPriority();
+
+      switch (handler->GetState())
+      {
+        case JobState_Pending:
+        case JobState_Retry:
+        case JobState_Running:
+          handler->SetState(JobState_Pending);
+          pendingJobs_.push(handler.get());
+          pendingJobAvailable_.notify_one();
+          break;
+ 
+        case JobState_Success:
+          SetCompletedJob(*handler, true);
+          break;
+        
+        case JobState_Failure:
+          SetCompletedJob(*handler, false);
+          break;
+
+        case JobState_Paused:
+          break;
+        
+        default:
+          LOG(ERROR) << "A job should not be loaded from state: "
+                     << EnumerationToString(handler->GetState());
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (keepLastChangeTime)
+      {
+        handler->SetLastStateChangeTime(lastChangeTime);
+      }
+    
+      jobsIndex_.insert(std::make_pair(id, handler.release()));
+
+      LOG(INFO) << "New job submitted with priority " << priority << ": " << id;
+
+      if (observer_ != NULL)
+      {
+        observer_->SignalJobSubmitted(id);
+      }
+
+      CheckInvariants();
+    }
+  }
+
+
+  void JobsRegistry::Submit(std::string& id,
+                            IJob* job,        // Takes ownership
+                            int priority)
+  {
+    SubmitInternal(id, new JobHandler(job, priority), false);
+  }
+
+
+  void JobsRegistry::Submit(IJob* job,        // Takes ownership
+                            int priority)
+  {
+    std::string id;
+    SubmitInternal(id, new JobHandler(job, priority), false);
+  }
+
+
+  bool JobsRegistry::SubmitAndWait(Json::Value& successContent,
+                                   IJob* job,        // Takes ownership
+                                   int priority)
+  {
+    std::string id;
+    Submit(id, job, priority);
+
+    JobState state = JobState_Pending;  // Dummy initialization
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      for (;;)
+      {
+        if (!GetStateInternal(state, id))
+        {
+          // Job has finished and has been lost (should not happen)
+          state = JobState_Failure;
+          break;
+        }
+        else if (state == JobState_Failure)
+        {
+          // Failure
+          break;
+        }
+        else if (state == JobState_Success)
+        {
+          // Success, try and retrieve the status of the job
+          JobsIndex::const_iterator it = jobsIndex_.find(id);
+          if (it == jobsIndex_.end())
+          {
+            // Should not happen
+            state = JobState_Failure;
+          }
+          else
+          {
+            const JobStatus& status = it->second->GetLastStatus();
+            successContent = status.GetPublicContent();
+          }
+          
+          break;
+        }
+        else
+        {
+          // This job has not finished yet, wait for new completion
+          someJobComplete_.wait(lock);
+        }
+      }
+    }
+
+    return (state == JobState_Success);
+  }
+
+
+  bool JobsRegistry::SetPriority(const std::string& id,
+                                 int priority)
+  {
+    LOG(INFO) << "Changing priority to " << priority << " for job: " << id;
+
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      LOG(WARNING) << "Unknown job: " << id;
+      return false;
+    }
+    else
+    {
+      found->second->SetPriority(priority);
+
+      if (found->second->GetState() == JobState_Pending)
+      {
+        // If the job is pending, we need to reconstruct the
+        // priority queue, as the heap condition has changed
+
+        PendingJobs copy;
+        std::swap(copy, pendingJobs_);
+
+        assert(pendingJobs_.empty());
+        while (!copy.empty())
+        {
+          pendingJobs_.push(copy.top());
+          copy.pop();
+        }
+      }
+
+      CheckInvariants();
+      return true;
+    }
+  }
+
+
+  void JobsRegistry::RemovePendingJob(const std::string& id)
+  {
+    // If the job is pending, we need to reconstruct the priority
+    // queue to remove it
+    PendingJobs copy;
+    std::swap(copy, pendingJobs_);
+
+    assert(pendingJobs_.empty());
+    while (!copy.empty())
+    {
+      if (copy.top()->GetId() != id)
+      {
+        pendingJobs_.push(copy.top());
+      }
+
+      copy.pop();
+    }
+  }
+
+
+  void JobsRegistry::RemoveRetryJob(JobHandler* handler)
+  {
+    RetryJobs::iterator item = retryJobs_.find(handler);
+    assert(item != retryJobs_.end());            
+    retryJobs_.erase(item);
+  }
+
+
+  bool JobsRegistry::Pause(const std::string& id)
+  {
+    LOG(INFO) << "Pausing job: " << id;
+
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      LOG(WARNING) << "Unknown job: " << id;
+      return false;
+    }
+    else
+    {
+      switch (found->second->GetState())
+      {
+        case JobState_Pending:
+          RemovePendingJob(id);
+          found->second->SetState(JobState_Paused);
+          break;
+
+        case JobState_Retry:
+          RemoveRetryJob(found->second);
+          found->second->SetState(JobState_Paused);
+          break;
+
+        case JobState_Paused:
+        case JobState_Success:
+        case JobState_Failure:
+          // Nothing to be done
+          break;
+
+        case JobState_Running:
+          found->second->SchedulePause();
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      CheckInvariants();
+      return true;
+    }
+  }
+
+
+  bool JobsRegistry::Cancel(const std::string& id)
+  {
+    LOG(INFO) << "Canceling job: " << id;
+
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      LOG(WARNING) << "Unknown job: " << id;
+      return false;
+    }
+    else
+    {
+      switch (found->second->GetState())
+      {
+        case JobState_Pending:
+          RemovePendingJob(id);
+          SetCompletedJob(*found->second, false);
+          found->second->SetLastErrorCode(ErrorCode_CanceledJob);
+          break;
+
+        case JobState_Retry:
+          RemoveRetryJob(found->second);
+          SetCompletedJob(*found->second, false);
+          found->second->SetLastErrorCode(ErrorCode_CanceledJob);
+          break;
+
+        case JobState_Paused:
+          SetCompletedJob(*found->second, false);
+          found->second->SetLastErrorCode(ErrorCode_CanceledJob);
+          break;
+        
+        case JobState_Success:
+        case JobState_Failure:
+          // Nothing to be done
+          break;
+
+        case JobState_Running:
+          found->second->ScheduleCancel();
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      CheckInvariants();
+      return true;
+    }
+  }
+
+
+  bool JobsRegistry::Resume(const std::string& id)
+  {
+    LOG(INFO) << "Resuming job: " << id;
+
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      LOG(WARNING) << "Unknown job: " << id;
+      return false;
+    }
+    else if (found->second->GetState() != JobState_Paused)
+    {
+      LOG(WARNING) << "Cannot resume a job that is not paused: " << id;
+      return false;
+    }
+    else
+    {
+      found->second->SetState(JobState_Pending);
+      pendingJobs_.push(found->second);
+      pendingJobAvailable_.notify_one();
+      CheckInvariants();
+      return true;      
+    }
+  }
+
+
+  bool JobsRegistry::Resubmit(const std::string& id)
+  {
+    LOG(INFO) << "Resubmitting failed job: " << id;
+
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      LOG(WARNING) << "Unknown job: " << id;
+      return false;
+    }
+    else if (found->second->GetState() != JobState_Failure)
+    {
+      LOG(WARNING) << "Cannot resubmit a job that has not failed: " << id;
+      return false;
+    }
+    else
+    {
+      found->second->GetJob().Reset();
+      
+      bool ok = false;
+      for (CompletedJobs::iterator it = completedJobs_.begin(); 
+           it != completedJobs_.end(); ++it)
+      {
+        if (*it == found->second)
+        {
+          ok = true;
+          completedJobs_.erase(it);
+          break;
+        }
+      }
+
+      assert(ok);
+
+      found->second->SetState(JobState_Pending);
+      pendingJobs_.push(found->second);
+      pendingJobAvailable_.notify_one();
+
+      CheckInvariants();
+      return true;
+    }
+  }
+
+
+  void JobsRegistry::ScheduleRetries()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    RetryJobs copy;
+    std::swap(copy, retryJobs_);
+
+    const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();
+
+    assert(retryJobs_.empty());
+    for (RetryJobs::iterator it = copy.begin(); it != copy.end(); ++it)
+    {
+      if ((*it)->IsRetryReady(now))
+      {
+        LOG(INFO) << "Retrying job: " << (*it)->GetId();
+        (*it)->SetState(JobState_Pending);
+        pendingJobs_.push(*it);
+        pendingJobAvailable_.notify_one();
+      }
+      else
+      {
+        retryJobs_.insert(*it);
+      }
+    }
+
+    CheckInvariants();
+  }
+
+
+  bool JobsRegistry::GetState(JobState& state,
+                              const std::string& id)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return GetStateInternal(state, id);
+  }
+
+
+  void JobsRegistry::SetObserver(JobsRegistry::IObserver& observer)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    observer_ = &observer;
+  }
+
+  
+  void JobsRegistry::ResetObserver()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    observer_ = NULL;
+  }
+
+  
+  JobsRegistry::RunningJob::RunningJob(JobsRegistry& registry,
+                                       unsigned int timeout) :
+    registry_(registry),
+    handler_(NULL),
+    targetState_(JobState_Failure),
+    targetRetryTimeout_(0),
+    canceled_(false)
+  {
+    {
+      boost::mutex::scoped_lock lock(registry_.mutex_);
+
+      while (registry_.pendingJobs_.empty())
+      {
+        if (timeout == 0)
+        {
+          registry_.pendingJobAvailable_.wait(lock);
+        }
+        else
+        {
+          bool success = registry_.pendingJobAvailable_.timed_wait
+            (lock, boost::posix_time::milliseconds(timeout));
+          if (!success)
+          {
+            // No pending job
+            return;
+          }
+        }
+      }
+
+      handler_ = registry_.pendingJobs_.top();
+      registry_.pendingJobs_.pop();
+
+      assert(handler_->GetState() == JobState_Pending);
+      handler_->SetState(JobState_Running);
+      handler_->SetLastErrorCode(ErrorCode_Success);
+
+      job_ = &handler_->GetJob();
+      id_ = handler_->GetId();
+      priority_ = handler_->GetPriority();
+    }
+  }
+
+      
+  JobsRegistry::RunningJob::~RunningJob()
+  {
+    if (IsValid())
+    {
+      boost::mutex::scoped_lock lock(registry_.mutex_);
+
+      switch (targetState_)
+      {
+        case JobState_Failure:
+          registry_.MarkRunningAsCompleted(*handler_, false);
+
+          if (canceled_)
+          {
+            handler_->SetLastErrorCode(ErrorCode_CanceledJob);
+          }
+          
+          break;
+
+        case JobState_Success:
+          registry_.MarkRunningAsCompleted(*handler_, true);
+          break;
+
+        case JobState_Paused:
+          registry_.MarkRunningAsPaused(*handler_);
+          break;            
+
+        case JobState_Retry:
+          registry_.MarkRunningAsRetry(*handler_, targetRetryTimeout_);
+          break;
+            
+        default:
+          assert(0);
+      }
+    }
+  }
+
+      
+  bool JobsRegistry::RunningJob::IsValid() const
+  {
+    return (handler_ != NULL &&
+            job_ != NULL);
+  }
+
+      
+  const std::string& JobsRegistry::RunningJob::GetId() const
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return id_;
+    }
+  }
+
+      
+  int JobsRegistry::RunningJob::GetPriority() const
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return priority_;
+    }
+  }
+      
+
+  IJob& JobsRegistry::RunningJob::GetJob()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *job_;
+    }
+  }
+
+      
+  bool JobsRegistry::RunningJob::IsPauseScheduled()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      boost::mutex::scoped_lock lock(registry_.mutex_);
+      registry_.CheckInvariants();
+      assert(handler_->GetState() == JobState_Running);
+        
+      return handler_->IsPauseScheduled();
+    }
+  }
+
+      
+  bool JobsRegistry::RunningJob::IsCancelScheduled()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      boost::mutex::scoped_lock lock(registry_.mutex_);
+      registry_.CheckInvariants();
+      assert(handler_->GetState() == JobState_Running);
+        
+      return handler_->IsCancelScheduled();
+    }
+  }
+
+      
+  void JobsRegistry::RunningJob::MarkSuccess()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      targetState_ = JobState_Success;
+    }
+  }
+
+      
+  void JobsRegistry::RunningJob::MarkFailure()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      targetState_ = JobState_Failure;
+    }
+  }
+
+      
+  void JobsRegistry::RunningJob::MarkCanceled()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      targetState_ = JobState_Failure;
+      canceled_ = true;
+    }
+  }
+
+      
+  void JobsRegistry::RunningJob::MarkPause()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      targetState_ = JobState_Paused;
+    }
+  }
+
+      
+  void JobsRegistry::RunningJob::MarkRetry(unsigned int timeout)
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      targetState_ = JobState_Retry;
+      targetRetryTimeout_ = timeout;
+    }
+  }
+      
+
+  void JobsRegistry::RunningJob::UpdateStatus(ErrorCode code)
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      JobStatus status(code, *job_);
+          
+      boost::mutex::scoped_lock lock(registry_.mutex_);
+      registry_.CheckInvariants();
+      assert(handler_->GetState() == JobState_Running);
+        
+      handler_->SetLastStatus(status);
+    }
+  }
+
+
+
+  void JobsRegistry::Serialize(Json::Value& target)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    target = Json::objectValue;
+    target[TYPE] = JOBS_REGISTRY;
+    target[MAX_COMPLETED_JOBS] = static_cast<unsigned int>(maxCompletedJobs_);
+    target[JOBS] = Json::objectValue;
+    
+    for (JobsIndex::const_iterator it = jobsIndex_.begin(); 
+         it != jobsIndex_.end(); ++it)
+    {
+      Json::Value v;
+      if (it->second->Serialize(v))
+      {
+        target[JOBS][it->first] = v;
+      }
+    }
+  }
+
+
+  JobsRegistry::JobsRegistry(IJobUnserializer& unserializer,
+                             const Json::Value& s) :
+    observer_(NULL)
+  {
+    if (SerializationToolbox::ReadString(s, TYPE) != JOBS_REGISTRY ||
+        !s.isMember(JOBS) ||
+        s[JOBS].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    maxCompletedJobs_ = SerializationToolbox::ReadUnsignedInteger(s, MAX_COMPLETED_JOBS);
+
+    Json::Value::Members members = s[JOBS].getMemberNames();
+
+    for (Json::Value::Members::const_iterator it = members.begin();
+         it != members.end(); ++it)
+    {
+      std::auto_ptr<JobHandler> job(new JobHandler(unserializer, s[JOBS][*it], *it));
+      
+      std::string id;
+      SubmitInternal(id, job.release(), true);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobsRegistry.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,235 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The job engine cannot be used in sandboxed environments
+#endif
+
+#include "JobInfo.h"
+#include "IJobUnserializer.h"
+
+#include <list>
+#include <set>
+#include <queue>
+#include <boost/thread/mutex.hpp>
+#include <boost/thread/condition_variable.hpp>
+
+namespace Orthanc
+{
+  // This class handles the state machine of the jobs engine
+  class JobsRegistry : public boost::noncopyable
+  {
+  public:
+    class IObserver : public boost::noncopyable
+    {
+    public:
+      virtual ~IObserver()
+      {
+      }
+
+      virtual void SignalJobSubmitted(const std::string& jobId) = 0;
+
+      virtual void SignalJobSuccess(const std::string& jobId) = 0;
+
+      virtual void SignalJobFailure(const std::string& jobId) = 0;
+    };
+    
+  private:
+    class JobHandler;
+
+    struct PriorityComparator
+    {
+      bool operator() (JobHandler*& a,
+                       JobHandler*& b) const;
+    };
+
+    typedef std::map<std::string, JobHandler*>              JobsIndex;
+    typedef std::list<JobHandler*>                          CompletedJobs;
+    typedef std::set<JobHandler*>                           RetryJobs;
+    typedef std::priority_queue<JobHandler*, 
+                                std::vector<JobHandler*>,   // Could be a "std::deque"
+                                PriorityComparator>         PendingJobs;
+
+    boost::mutex               mutex_;
+    JobsIndex                  jobsIndex_;
+    PendingJobs                pendingJobs_;
+    CompletedJobs              completedJobs_;
+    RetryJobs                  retryJobs_;
+
+    boost::condition_variable  pendingJobAvailable_;
+    boost::condition_variable  someJobComplete_;
+    size_t                     maxCompletedJobs_;
+
+    IObserver*                 observer_;
+
+
+#ifndef NDEBUG
+    bool IsPendingJob(const JobHandler& job) const;
+
+    bool IsCompletedJob(JobHandler& job) const;
+    
+    bool IsRetryJob(JobHandler& job) const;
+#endif
+
+    void CheckInvariants() const;
+
+    void ForgetOldCompletedJobs();
+
+    void SetCompletedJob(JobHandler& job,
+                         bool success);
+    
+    void MarkRunningAsCompleted(JobHandler& job,
+                                bool success);
+
+    void MarkRunningAsRetry(JobHandler& job,
+                            unsigned int timeout);
+    
+    void MarkRunningAsPaused(JobHandler& job);
+    
+    bool GetStateInternal(JobState& state,
+                          const std::string& id);
+
+    void RemovePendingJob(const std::string& id);
+      
+    void RemoveRetryJob(JobHandler* handler);
+      
+    void SubmitInternal(std::string& id,
+                        JobHandler* handler,
+                        bool keepLastChangeTime);
+    
+  public:
+    JobsRegistry() :
+      maxCompletedJobs_(10),
+      observer_(NULL)
+    {
+    }
+
+    JobsRegistry(IJobUnserializer& unserializer,
+                 const Json::Value& s);
+
+    ~JobsRegistry();
+
+    void SetMaxCompletedJobs(size_t i);
+    
+    void ListJobs(std::set<std::string>& target);
+
+    bool GetJobInfo(JobInfo& target,
+                    const std::string& id);
+
+    void Serialize(Json::Value& target);
+    
+    void Submit(std::string& id,
+                IJob* job,        // Takes ownership
+                int priority);
+    
+    void Submit(IJob* job,        // Takes ownership
+                int priority);
+
+    bool SubmitAndWait(Json::Value& successContent,
+                       IJob* job,        // Takes ownership
+                       int priority);
+    
+    bool SetPriority(const std::string& id,
+                     int priority);
+
+    bool Pause(const std::string& id);
+    
+    bool Resume(const std::string& id);
+
+    bool Resubmit(const std::string& id);
+
+    bool Cancel(const std::string& id);
+    
+    void ScheduleRetries();
+    
+    bool GetState(JobState& state,
+                  const std::string& id);
+
+    void SetObserver(IObserver& observer);
+
+    void ResetObserver();
+
+    class RunningJob : public boost::noncopyable
+    {
+    private:
+      JobsRegistry&  registry_;
+      JobHandler*    handler_;  // Can only be accessed if the
+                                // registry mutex is locked!
+      IJob*          job_;  // Will by design be in mutual exclusion,
+                            // because only one RunningJob can be
+                            // executed at a time on a JobHandler
+
+      std::string    id_;
+      int            priority_;
+      JobState       targetState_;
+      unsigned int   targetRetryTimeout_;
+      bool           canceled_;
+      
+    public:
+      RunningJob(JobsRegistry& registry,
+                 unsigned int timeout);
+
+      ~RunningJob();
+
+      bool IsValid() const;
+
+      const std::string& GetId() const;
+
+      int GetPriority() const;
+
+      IJob& GetJob();
+
+      bool IsPauseScheduled();
+
+      bool IsCancelScheduled();
+
+      void MarkSuccess();
+
+      void MarkFailure();
+
+      void MarkPause();
+
+      void MarkCanceled();
+
+      void MarkRetry(unsigned int timeout);
+
+      void UpdateStatus(ErrorCode code);
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/IJobOperation.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,54 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "JobOperationValues.h"
+#include "../../DicomNetworking/IDicomConnectionManager.h"
+
+namespace Orthanc
+{
+  class IJobOperation : public boost::noncopyable
+  {
+  public:
+    virtual ~IJobOperation()
+    {
+    }
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& dicomConnection) = 0;
+
+    virtual void Serialize(Json::Value& result) const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/JobOperationValue.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <json/value.h>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class JobOperationValue : public boost::noncopyable
+  {
+  public:
+    enum Type
+    {
+      Type_DicomInstance,
+      Type_Null,
+      Type_String
+    };
+
+  private:
+    Type  type_;
+
+  protected:
+    JobOperationValue(Type type) :
+      type_(type)
+    {
+    }
+
+  public:
+    virtual ~JobOperationValue()
+    {
+    }
+
+    Type GetType() const
+    {
+      return type_;
+    }
+
+    virtual JobOperationValue* Clone() const = 0;
+
+    virtual void Serialize(Json::Value& target) const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/JobOperationValues.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,143 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "JobOperationValues.h"
+
+#include "../IJobUnserializer.h"
+#include "../../OrthancException.h"
+
+#include <cassert>
+#include <memory>
+
+namespace Orthanc
+{
+  void JobOperationValues::Append(JobOperationValues& target,
+                                  bool clear)
+  {
+    target.Reserve(target.GetSize() + GetSize());
+
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      if (clear)
+      {
+        target.Append(values_[i]);
+        values_[i] = NULL;
+      }
+      else
+      {
+        target.Append(GetValue(i).Clone());
+      }
+    }
+
+    if (clear)
+    {
+      Clear();
+    }
+  }
+
+
+  void JobOperationValues::Clear()
+  {
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      if (values_[i] != NULL)
+      {
+        delete values_[i];
+      }
+    }
+
+    values_.clear();
+  }
+
+
+  void JobOperationValues::Append(JobOperationValue* value)  // Takes ownership
+  {
+    if (value == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      values_.push_back(value);
+    }
+  }
+
+
+  JobOperationValue& JobOperationValues::GetValue(size_t index) const
+  {
+    if (index >= values_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(values_[index] != NULL);
+      return *values_[index];
+    }
+  }
+
+
+  void JobOperationValues::Serialize(Json::Value& target) const
+  {
+    target = Json::arrayValue;
+
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      Json::Value tmp;
+      values_[i]->Serialize(tmp);
+      target.append(tmp);
+    }
+  }
+
+
+  JobOperationValues* JobOperationValues::Unserialize(IJobUnserializer& unserializer,
+                                                      const Json::Value& source)
+  {
+    if (source.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    std::auto_ptr<JobOperationValues> result(new JobOperationValues);
+
+    result->Reserve(source.size());
+    
+    for (Json::Value::ArrayIndex i = 0; i < source.size(); i++)
+    {
+      result->Append(unserializer.UnserializeValue(source[i]));
+    }
+    
+    return result.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/JobOperationValues.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,89 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "JobOperationValue.h"
+
+#include <vector>
+
+namespace Orthanc
+{
+  class IJobUnserializer;
+
+  class JobOperationValues : public boost::noncopyable
+  {
+  private:
+    std::vector<JobOperationValue*>   values_;
+
+    void Append(JobOperationValues& target,
+                bool clear);
+
+  public:
+    ~JobOperationValues()
+    {
+      Clear();
+    }
+
+    void Move(JobOperationValues& target)
+    {
+      return Append(target, true);
+    }
+
+    void Copy(JobOperationValues& target)
+    {
+      return Append(target, false);
+    }
+
+    void Clear();
+
+    void Reserve(size_t count)
+    {
+      values_.reserve(count);
+    }
+
+    void Append(JobOperationValue* value);  // Takes ownership
+
+    size_t GetSize() const
+    {
+      return values_.size();
+    }
+
+    JobOperationValue& GetValue(size_t index) const;
+
+    void Serialize(Json::Value& target) const;
+
+    static JobOperationValues* Unserialize(IJobUnserializer& unserializer,
+                                           const Json::Value& source);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/LogJobOperation.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,64 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "LogJobOperation.h"
+
+#include "../../Logging.h"
+#include "StringOperationValue.h"
+
+namespace Orthanc
+{
+  void LogJobOperation::Apply(JobOperationValues& outputs,
+                              const JobOperationValue& input,
+                              IDicomConnectionManager& connectionManager)
+  {
+    switch (input.GetType())
+    {
+      case JobOperationValue::Type_String:
+        LOG(INFO) << "Job value: "
+                  << dynamic_cast<const StringOperationValue&>(input).GetContent();
+        break;
+
+      case JobOperationValue::Type_Null:
+        LOG(INFO) << "Job value: (null)";
+        break;
+
+      default:
+        LOG(INFO) << "Job value: (unsupport)";
+        break;
+    }
+
+    outputs.Append(input.Clone());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/LogJobOperation.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IJobOperation.h"
+
+namespace Orthanc
+{
+  class LogJobOperation : public IJobOperation
+  {
+  public:
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& connectionManager);
+
+    virtual void Serialize(Json::Value& result) const
+    {
+      result = Json::objectValue;
+      result["Type"] = "Log";
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/NullOperationValue.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "JobOperationValue.h"
+
+namespace Orthanc
+{
+  class NullOperationValue : public JobOperationValue
+  {
+  public:
+    NullOperationValue() :
+      JobOperationValue(Type_Null)
+    {
+    }
+
+    virtual JobOperationValue* Clone() const
+    {
+      return new NullOperationValue;
+    }
+
+    virtual void Serialize(Json::Value& target) const
+    {
+      target = Json::objectValue;
+      target["Type"] = "Null";
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,495 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "SequenceOfOperationsJob.h"
+
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+#include "../../SerializationToolbox.h"
+#include "../IJobUnserializer.h"
+
+namespace Orthanc
+{
+  static const char* CURRENT = "Current";
+  static const char* DESCRIPTION = "Description";
+  static const char* DICOM_TIMEOUT = "DicomTimeout";
+  static const char* NEXT_OPERATIONS = "Next";
+  static const char* OPERATION = "Operation";
+  static const char* OPERATIONS = "Operations";
+  static const char* ORIGINAL_INPUTS = "OriginalInputs";
+  static const char* TRAILING_TIMEOUT = "TrailingTimeout";
+  static const char* TYPE = "Type";
+  static const char* WORK_INPUTS = "WorkInputs";
+
+  
+  class SequenceOfOperationsJob::Operation : public boost::noncopyable
+  {
+  private:
+    size_t                             index_;
+    std::auto_ptr<IJobOperation>       operation_;
+    std::auto_ptr<JobOperationValues>  originalInputs_;
+    std::auto_ptr<JobOperationValues>  workInputs_;
+    std::list<Operation*>              nextOperations_;
+    size_t                             currentInput_;
+
+  public:
+    Operation(size_t index,
+              IJobOperation* operation) :
+      index_(index),
+      operation_(operation),
+      originalInputs_(new JobOperationValues),
+      workInputs_(new JobOperationValues),
+      currentInput_(0)
+    {
+      if (operation == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+    }
+
+    void AddOriginalInput(const JobOperationValue& value)
+    {
+      if (currentInput_ != 0)
+      {
+        // Cannot add input after processing has started
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        originalInputs_->Append(value.Clone());
+      }
+    }
+
+    const JobOperationValues& GetOriginalInputs() const
+    {
+      return *originalInputs_;
+    }
+
+    void Reset()
+    {
+      workInputs_->Clear();
+      currentInput_ = 0;
+    }
+
+    void AddNextOperation(Operation& other,
+                          bool unserializing)
+    {
+      if (other.index_ <= index_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (!unserializing &&
+          currentInput_ != 0)
+      {
+        // Cannot add input after processing has started
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        nextOperations_.push_back(&other);
+      }
+    }
+
+    bool IsDone() const
+    {
+      return currentInput_ >= originalInputs_->GetSize() + workInputs_->GetSize();
+    }
+
+    void Step(IDicomConnectionManager& connectionManager)
+    {
+      if (IsDone())
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      const JobOperationValue* input;
+
+      if (currentInput_ < originalInputs_->GetSize())
+      {
+        input = &originalInputs_->GetValue(currentInput_);
+      }
+      else
+      {
+        input = &workInputs_->GetValue(currentInput_ - originalInputs_->GetSize());
+      }
+
+      JobOperationValues outputs;
+      operation_->Apply(outputs, *input, connectionManager);
+
+      if (!nextOperations_.empty())
+      {
+        std::list<Operation*>::iterator first = nextOperations_.begin();
+        outputs.Move(*(*first)->workInputs_);
+
+        std::list<Operation*>::iterator current = first;
+        ++current;
+
+        while (current != nextOperations_.end())
+        {
+          (*first)->workInputs_->Copy(*(*current)->workInputs_);
+          ++current;
+        }
+      }
+
+      currentInput_ += 1;
+    }
+
+    void Serialize(Json::Value& target) const
+    {
+      target = Json::objectValue;
+      target[CURRENT] = static_cast<unsigned int>(currentInput_);
+      operation_->Serialize(target[OPERATION]);
+      originalInputs_->Serialize(target[ORIGINAL_INPUTS]);
+      workInputs_->Serialize(target[WORK_INPUTS]);      
+
+      Json::Value tmp = Json::arrayValue;
+      for (std::list<Operation*>::const_iterator it = nextOperations_.begin();
+           it != nextOperations_.end(); ++it)
+      {
+        tmp.append(static_cast<int>((*it)->index_));
+      }
+
+      target[NEXT_OPERATIONS] = tmp;
+    }
+
+    Operation(IJobUnserializer& unserializer,
+              Json::Value::ArrayIndex index,
+              const Json::Value& serialized) :
+      index_(index)
+    {
+      if (serialized.type() != Json::objectValue ||
+          !serialized.isMember(OPERATION) ||
+          !serialized.isMember(ORIGINAL_INPUTS) ||
+          !serialized.isMember(WORK_INPUTS))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      currentInput_ = SerializationToolbox::ReadUnsignedInteger(serialized, CURRENT);
+      operation_.reset(unserializer.UnserializeOperation(serialized[OPERATION]));
+      originalInputs_.reset(JobOperationValues::Unserialize
+                            (unserializer, serialized[ORIGINAL_INPUTS]));
+      workInputs_.reset(JobOperationValues::Unserialize
+                        (unserializer, serialized[WORK_INPUTS]));
+    }
+  };
+
+
+  SequenceOfOperationsJob::SequenceOfOperationsJob() :
+    done_(false),
+    current_(0),
+    trailingTimeout_(boost::posix_time::milliseconds(1000))
+  {
+  }
+
+
+  SequenceOfOperationsJob::~SequenceOfOperationsJob()
+  {
+    for (size_t i = 0; i < operations_.size(); i++)
+    {
+      if (operations_[i] != NULL)
+      {
+        delete operations_[i];
+      }
+    }
+  }
+
+
+  void SequenceOfOperationsJob::SetDescription(const std::string& description)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    description_ = description;
+  }
+
+
+  void SequenceOfOperationsJob::GetDescription(std::string& description)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    description = description_;    
+  }
+
+
+  void SequenceOfOperationsJob::Register(IObserver& observer)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    observers_.push_back(&observer);
+  }
+
+
+  void SequenceOfOperationsJob::Lock::SetTrailingOperationTimeout(unsigned int timeout)
+  {
+    that_.trailingTimeout_ = boost::posix_time::milliseconds(timeout);
+  }
+
+  
+  void SequenceOfOperationsJob::Lock::SetDicomAssociationTimeout(unsigned int timeout)
+  {
+    that_.connectionManager_.SetTimeout(timeout);
+  }
+
+
+  size_t SequenceOfOperationsJob::Lock::AddOperation(IJobOperation* operation)
+  {
+    if (IsDone())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    size_t index = that_.operations_.size();
+
+    that_.operations_.push_back(new Operation(index, operation));
+    that_.operationAdded_.notify_one();
+
+    return index;
+  }
+
+
+  void SequenceOfOperationsJob::Lock::AddInput(size_t index,
+                                               const JobOperationValue& value)
+  {
+    if (IsDone())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (index >= that_.operations_.size() ||
+             index < that_.current_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      that_.operations_[index]->AddOriginalInput(value);
+    }
+  }
+      
+
+  void SequenceOfOperationsJob::Lock::Connect(size_t input,
+                                              size_t output)
+  {
+    if (IsDone())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (input >= output ||
+             input >= that_.operations_.size() ||
+             output >= that_.operations_.size() ||
+             input < that_.current_ ||
+             output < that_.current_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      Operation& a = *that_.operations_[input];
+      Operation& b = *that_.operations_[output];
+      a.AddNextOperation(b, false /* not unserializing */);
+    }
+  }
+
+
+  JobStepResult SequenceOfOperationsJob::Step()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (current_ == operations_.size())
+    {
+      LOG(INFO) << "Executing the trailing timeout in the sequence of operations";
+      operationAdded_.timed_wait(lock, trailingTimeout_);
+            
+      if (current_ == operations_.size())
+      {
+        // No operation was added during the trailing timeout: The
+        // job is over
+        LOG(INFO) << "The sequence of operations is over";
+        done_ = true;
+
+        for (std::list<IObserver*>::iterator it = observers_.begin(); 
+             it != observers_.end(); ++it)
+        {
+          (*it)->SignalDone(*this);
+        }
+
+        connectionManager_.Close();
+        return JobStepResult::Success();
+      }
+      else
+      {
+        LOG(INFO) << "New operation were added to the sequence of operations";
+      }
+    }
+
+    assert(current_ < operations_.size());
+
+    while (current_ < operations_.size() &&
+           operations_[current_]->IsDone())
+    {
+      current_++;
+    }
+
+    if (current_ < operations_.size())
+    {
+      operations_[current_]->Step(connectionManager_);
+    }
+
+    connectionManager_.CheckTimeout();
+
+    return JobStepResult::Continue();
+  }
+
+
+  void SequenceOfOperationsJob::Reset()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+      
+    current_ = 0;
+    done_ = false;
+
+    for (size_t i = 0; i < operations_.size(); i++)
+    {
+      operations_[i]->Reset();
+    }
+  }
+
+
+  void SequenceOfOperationsJob::Stop(JobStopReason reason)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    connectionManager_.Close();
+  }
+
+
+  float SequenceOfOperationsJob::GetProgress()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+      
+    return (static_cast<float>(current_) / 
+            static_cast<float>(operations_.size() + 1));
+  }
+
+
+  void SequenceOfOperationsJob::GetPublicContent(Json::Value& value)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    value["CountOperations"] = static_cast<unsigned int>(operations_.size());
+    value["Description"] = description_;
+  }
+
+
+  bool SequenceOfOperationsJob::Serialize(Json::Value& value)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    value = Json::objectValue;
+
+    std::string jobType;
+    GetJobType(jobType);
+    value[TYPE] = jobType;
+    
+    value[DESCRIPTION] = description_;
+    value[TRAILING_TIMEOUT] = static_cast<unsigned int>(trailingTimeout_.total_milliseconds());
+    value[DICOM_TIMEOUT] = connectionManager_.GetTimeout();
+    value[CURRENT] = static_cast<unsigned int>(current_);
+    
+    Json::Value tmp = Json::arrayValue;
+    for (size_t i = 0; i < operations_.size(); i++)
+    {
+      Json::Value operation = Json::objectValue;
+      operations_[i]->Serialize(operation);
+      tmp.append(operation);
+    }
+
+    value[OPERATIONS] = tmp;
+
+    return true;
+  }
+
+
+  SequenceOfOperationsJob::SequenceOfOperationsJob(IJobUnserializer& unserializer,
+                                                   const Json::Value& serialized) :
+    done_(false)
+  {
+    std::string jobType;
+    GetJobType(jobType);
+    
+    if (SerializationToolbox::ReadString(serialized, TYPE) != jobType ||
+        !serialized.isMember(OPERATIONS) ||
+        serialized[OPERATIONS].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    description_ = SerializationToolbox::ReadString(serialized, DESCRIPTION);
+    trailingTimeout_ = boost::posix_time::milliseconds
+      (SerializationToolbox::ReadUnsignedInteger(serialized, TRAILING_TIMEOUT));
+    connectionManager_.SetTimeout
+      (SerializationToolbox::ReadUnsignedInteger(serialized, DICOM_TIMEOUT));
+    current_ = SerializationToolbox::ReadUnsignedInteger(serialized, CURRENT);
+
+    const Json::Value& ops = serialized[OPERATIONS];
+
+    // Unserialize the individual operations
+    operations_.reserve(ops.size());
+    for (Json::Value::ArrayIndex i = 0; i < ops.size(); i++)
+    {
+      operations_.push_back(new Operation(unserializer, i, ops[i]));
+    }
+
+    // Connect the next operations
+    for (Json::Value::ArrayIndex i = 0; i < ops.size(); i++)
+    {
+      if (!ops[i].isMember(NEXT_OPERATIONS) ||
+          ops[i][NEXT_OPERATIONS].type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      const Json::Value& next = ops[i][NEXT_OPERATIONS];
+      for (Json::Value::ArrayIndex j = 0; j < next.size(); j++)
+      {
+        if (next[j].type() != Json::intValue ||
+            next[j].asInt() < 0 ||
+            next[j].asUInt() >= operations_.size())
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          operations_[i]->AddNextOperation(*operations_[next[j].asUInt()], true);
+        }
+      }
+    }  
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,153 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../IJob.h"
+#include "IJobOperation.h"
+
+#include "../../DicomNetworking/TimeoutDicomConnectionManager.h"
+
+#include <boost/thread/mutex.hpp>
+#include <boost/thread/condition_variable.hpp>
+
+#include <list>
+
+namespace Orthanc
+{
+  class SequenceOfOperationsJob : public IJob
+  {
+  public:
+    class IObserver : public boost::noncopyable
+    {
+    public:
+      virtual ~IObserver()
+      {
+      }
+
+      virtual void SignalDone(const SequenceOfOperationsJob& job) = 0;
+    };
+
+  private:
+    class Operation;
+
+    std::string                       description_;
+    bool                              done_;
+    boost::mutex                      mutex_;
+    std::vector<Operation*>           operations_;
+    size_t                            current_;
+    boost::condition_variable         operationAdded_;
+    boost::posix_time::time_duration  trailingTimeout_;
+    std::list<IObserver*>             observers_;
+    TimeoutDicomConnectionManager     connectionManager_;
+
+  public:
+    SequenceOfOperationsJob();
+
+    SequenceOfOperationsJob(IJobUnserializer& unserializer,
+                            const Json::Value& serialized);
+
+    virtual ~SequenceOfOperationsJob();
+
+    void SetDescription(const std::string& description);
+
+    void GetDescription(std::string& description);
+
+    void Register(IObserver& observer);
+
+    // This lock allows adding new operations to the end of the job,
+    // from another thread than the worker thread, after the job has
+    // been submitted for processing
+    class Lock : public boost::noncopyable
+    {
+    private:
+      SequenceOfOperationsJob&   that_;
+      boost::mutex::scoped_lock  lock_;
+
+    public:
+      Lock(SequenceOfOperationsJob& that) :
+      that_(that),
+      lock_(that.mutex_)
+      {
+      }
+
+      bool IsDone() const
+      {
+        return that_.done_;
+      }
+
+      void SetTrailingOperationTimeout(unsigned int timeout);
+
+      void SetDicomAssociationTimeout(unsigned int timeout);
+      
+      size_t AddOperation(IJobOperation* operation);
+
+      size_t GetOperationsCount() const
+      {
+        return that_.operations_.size();
+      }
+
+      void AddInput(size_t index,
+                    const JobOperationValue& value);
+      
+      void Connect(size_t input,
+                   size_t output);
+    };
+
+    virtual void Start()
+    {
+    }
+
+    virtual JobStepResult Step();
+
+    virtual void Reset();
+
+    virtual void Stop(JobStopReason reason);
+
+    virtual float GetProgress();
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = "SequenceOfOperations";
+    }
+
+    virtual void GetPublicContent(Json::Value& value);
+
+    virtual bool Serialize(Json::Value& value);
+
+    void AwakeTrailingSleep()
+    {
+      operationAdded_.notify_one();
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/StringOperationValue.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,71 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "JobOperationValue.h"
+
+#include <string>
+
+namespace Orthanc
+{
+  class StringOperationValue : public JobOperationValue
+  {
+  private:
+    std::string  content_;
+
+  public:
+    StringOperationValue(const std::string& content) :
+      JobOperationValue(JobOperationValue::Type_String),
+      content_(content)
+    {
+    }
+
+    virtual JobOperationValue* Clone() const
+    {
+      return new StringOperationValue(content_);
+    }
+
+    const std::string& GetContent() const
+    {
+      return content_;
+    }
+
+    virtual void Serialize(Json::Value& target) const
+    {
+      target = Json::objectValue;
+      target["Type"] = "String";
+      target["Content"] = content_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/SetOfCommandsJob.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,303 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "SetOfCommandsJob.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+
+#include <cassert>
+#include <memory>
+
+namespace Orthanc
+{
+  SetOfCommandsJob::SetOfCommandsJob() :
+    started_(false),
+    permissive_(false),
+    position_(0)
+  {
+  }
+
+
+  SetOfCommandsJob::~SetOfCommandsJob()
+  {
+    for (size_t i = 0; i < commands_.size(); i++)
+    {
+      assert(commands_[i] != NULL);
+      delete commands_[i];
+    }
+  }
+
+    
+  void SetOfCommandsJob::Reserve(size_t size)
+  {
+    if (started_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      commands_.reserve(size);
+    }
+  }
+
+    
+  void SetOfCommandsJob::AddCommand(ICommand* command)
+  {
+    if (command == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (started_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      commands_.push_back(command);
+    }
+  }
+
+
+  void SetOfCommandsJob::SetPermissive(bool permissive)
+  {
+    if (started_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      permissive_ = permissive;
+    }
+  }
+
+
+  void SetOfCommandsJob::Reset()
+  {
+    if (started_)
+    {
+      position_ = 0;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+    
+  float SetOfCommandsJob::GetProgress()
+  {
+    if (commands_.empty())
+    {
+      return 1;
+    }
+    else
+    {
+      return (static_cast<float>(position_) /
+              static_cast<float>(commands_.size()));
+    }
+  }
+
+
+  const SetOfCommandsJob::ICommand& SetOfCommandsJob::GetCommand(size_t index) const
+  {
+    if (index >= commands_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(commands_[index] != NULL);
+      return *commands_[index];
+    }
+  }
+      
+
+  JobStepResult SetOfCommandsJob::Step()
+  {
+    if (!started_)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (commands_.empty() &&
+        position_ == 0)
+    {
+      // No command to handle: We're done
+      position_ = 1;
+      return JobStepResult::Success();
+    }
+    
+    if (position_ >= commands_.size())
+    {
+      // Already done
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    try
+    {
+      // Not at the trailing step: Handle the current command
+      if (!commands_[position_]->Execute())
+      {
+        // Error
+        if (!permissive_)
+        {
+          return JobStepResult::Failure(ErrorCode_InternalError);
+        }
+      }
+    }
+    catch (OrthancException& e)
+    {
+      if (permissive_)
+      {
+        LOG(WARNING) << "Ignoring an error in a permissive job: " << e.What();
+      }
+      else
+      {
+        return JobStepResult::Failure(e.GetErrorCode());
+      }
+    }
+
+    position_ += 1;
+
+    if (position_ == commands_.size())
+    {
+      // We're done
+      return JobStepResult::Success();
+    }
+    else
+    {
+      return JobStepResult::Continue();
+    }
+  }
+
+
+
+  static const char* KEY_DESCRIPTION = "Description";
+  static const char* KEY_PERMISSIVE = "Permissive";
+  static const char* KEY_POSITION = "Position";
+  static const char* KEY_TYPE = "Type";
+  static const char* KEY_COMMANDS = "Commands";
+
+  
+  void SetOfCommandsJob::GetPublicContent(Json::Value& value)
+  {
+    value[KEY_DESCRIPTION] = GetDescription();
+  }    
+
+
+  bool SetOfCommandsJob::Serialize(Json::Value& target)
+  {
+    target = Json::objectValue;
+
+    std::string type;
+    GetJobType(type);
+    target[KEY_TYPE] = type;
+    
+    target[KEY_PERMISSIVE] = permissive_;
+    target[KEY_POSITION] = static_cast<unsigned int>(position_);
+    target[KEY_DESCRIPTION] = description_;
+
+    target[KEY_COMMANDS] = Json::arrayValue;
+    Json::Value& tmp = target[KEY_COMMANDS];
+
+    for (size_t i = 0; i < commands_.size(); i++)
+    {
+      assert(commands_[i] != NULL);
+      
+      Json::Value command;
+      commands_[i]->Serialize(command);
+      tmp.append(command);
+    }
+
+    return true;
+  }
+
+
+  SetOfCommandsJob::SetOfCommandsJob(ICommandUnserializer* unserializer,
+                                     const Json::Value& source) :
+    started_(false)
+  {
+    std::auto_ptr<ICommandUnserializer> raii(unserializer);
+
+    permissive_ = SerializationToolbox::ReadBoolean(source, KEY_PERMISSIVE);
+    position_ = SerializationToolbox::ReadUnsignedInteger(source, KEY_POSITION);
+    description_ = SerializationToolbox::ReadString(source, KEY_DESCRIPTION);
+    
+    if (!source.isMember(KEY_COMMANDS) ||
+        source[KEY_COMMANDS].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      const Json::Value& tmp = source[KEY_COMMANDS];
+      commands_.resize(tmp.size());
+
+      for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++)
+      {
+        try
+        {
+          commands_[i] = unserializer->Unserialize(tmp[i]);
+        }
+        catch (OrthancException&)
+        {
+        }
+
+        if (commands_[i] == NULL)
+        {
+          for (size_t j = 0; j < i; j++)
+          {
+            delete commands_[j];
+          }
+
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+      }
+    }
+
+    if (commands_.empty())
+    {
+      if (position_ > 1)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+    else if (position_ > commands_.size())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/SetOfCommandsJob.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,135 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IJob.h"
+
+#include <set>
+
+namespace Orthanc
+{
+  class SetOfCommandsJob : public IJob
+  {
+  public:
+    class ICommand : public boost::noncopyable
+    {
+    public:
+      virtual ~ICommand()
+      {
+      }
+
+      virtual bool Execute() = 0;
+
+      virtual void Serialize(Json::Value& target) const = 0;
+    };
+
+    class ICommandUnserializer : public boost::noncopyable
+    {
+    public:
+      virtual ~ICommandUnserializer()
+      {
+      }
+      
+      virtual ICommand* Unserialize(const Json::Value& source) const = 0;
+    };
+    
+  private:
+    bool                    started_;
+    std::vector<ICommand*>  commands_;
+    bool                    permissive_;
+    size_t                  position_;
+    std::string             description_;
+
+  public:
+    SetOfCommandsJob();
+
+    SetOfCommandsJob(ICommandUnserializer* unserializer  /* takes ownership */,
+                     const Json::Value& source);
+
+    virtual ~SetOfCommandsJob();
+
+    size_t GetPosition() const
+    {
+      return position_;
+    }
+
+    void SetDescription(const std::string& description)
+    {
+      description_ = description;
+    }
+
+    const std::string& GetDescription() const
+    {
+      return description_;
+    }
+
+    void Reserve(size_t size);
+
+    size_t GetCommandsCount() const
+    {
+      return commands_.size();
+    }
+
+    void AddCommand(ICommand* command);  // Takes ownership
+
+    bool IsPermissive() const
+    {
+      return permissive_;
+    }
+
+    void SetPermissive(bool permissive);
+
+    virtual void Reset();
+    
+    virtual void Start()
+    {
+      started_ = true;
+    }
+    
+    virtual float GetProgress();
+
+    bool IsStarted() const
+    {
+      return started_;
+    }
+
+    const ICommand& GetCommand(size_t index) const;
+      
+    virtual JobStepResult Step();
+    
+    virtual void GetPublicContent(Json::Value& value);
+    
+    virtual bool Serialize(Json::Value& target);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/SetOfInstancesJob.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,240 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "SetOfInstancesJob.h"
+
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+
+#include <cassert>
+
+namespace Orthanc
+{
+  class SetOfInstancesJob::InstanceCommand : public SetOfInstancesJob::ICommand
+  {
+  private:
+    SetOfInstancesJob& that_;
+    std::string        instance_;
+
+  public:
+    InstanceCommand(SetOfInstancesJob& that,
+                    const std::string& instance) :
+      that_(that),
+      instance_(instance)
+    {
+    }
+
+    const std::string& GetInstance() const
+    {
+      return instance_;
+    }
+      
+    virtual bool Execute()
+    {
+      if (!that_.HandleInstance(instance_))
+      {
+        that_.failedInstances_.insert(instance_);
+        return false;
+      }
+      else
+      {
+        return true;
+      }
+    }
+
+    virtual void Serialize(Json::Value& target) const
+    {
+      target = instance_;
+    }
+  };
+
+
+  class SetOfInstancesJob::TrailingStepCommand : public SetOfInstancesJob::ICommand
+  {
+  private:
+    SetOfInstancesJob& that_;
+
+  public:
+    TrailingStepCommand(SetOfInstancesJob& that) :
+      that_(that)
+    {
+    }       
+      
+    virtual bool Execute()
+    {
+      return that_.HandleTrailingStep();
+    }
+
+    virtual void Serialize(Json::Value& target) const
+    {
+      target = Json::nullValue;
+    }
+  };
+
+
+  class SetOfInstancesJob::InstanceUnserializer :
+    public SetOfInstancesJob::ICommandUnserializer
+  {
+  private:
+    SetOfInstancesJob& that_;
+
+  public:
+    InstanceUnserializer(SetOfInstancesJob& that) :
+      that_(that)
+    {
+    }
+
+    virtual ICommand* Unserialize(const Json::Value& source) const
+    {
+      if (source.type() == Json::nullValue)
+      {
+        return new TrailingStepCommand(that_);
+      }
+      else if (source.type() == Json::stringValue)
+      {
+        return new InstanceCommand(that_, source.asString());
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+  };
+    
+
+  SetOfInstancesJob::SetOfInstancesJob() :
+    hasTrailingStep_(false)
+  {
+  }
+
+    
+  void SetOfInstancesJob::AddInstance(const std::string& instance)
+  {
+    AddCommand(new InstanceCommand(*this, instance));
+  }
+
+
+  void SetOfInstancesJob::AddTrailingStep()
+  {
+    AddCommand(new TrailingStepCommand(*this));
+    hasTrailingStep_ = true;
+  }
+  
+  
+  size_t SetOfInstancesJob::GetInstancesCount() const
+  {
+    if (hasTrailingStep_)
+    {
+      assert(GetCommandsCount() > 0);
+      return GetCommandsCount() - 1;
+    }
+    else
+    {
+      return GetCommandsCount();
+    }
+  }
+
+  
+  const std::string& SetOfInstancesJob::GetInstance(size_t index) const
+  {
+    if (index >= GetInstancesCount())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return dynamic_cast<const InstanceCommand&>(GetCommand(index)).GetInstance();
+    }
+  }
+
+
+  void SetOfInstancesJob::Start()
+  {
+    SetOfCommandsJob::Start();    
+  }
+
+
+  void SetOfInstancesJob::Reset()
+  {
+    SetOfCommandsJob::Reset();
+
+    failedInstances_.clear();
+  }
+
+
+  void SetOfInstancesJob::GetPublicContent(Json::Value& target)
+  {
+    SetOfCommandsJob::GetPublicContent(target);
+    target["InstancesCount"] = static_cast<uint32_t>(GetInstancesCount());
+    target["FailedInstancesCount"] = static_cast<uint32_t>(failedInstances_.size());
+  }    
+
+
+
+  static const char* KEY_TRAILING_STEP = "TrailingStep";
+  static const char* KEY_FAILED_INSTANCES = "FailedInstances";
+
+  bool SetOfInstancesJob::Serialize(Json::Value& target) 
+  {
+    if (SetOfCommandsJob::Serialize(target))
+    {
+      target[KEY_TRAILING_STEP] = hasTrailingStep_;
+      SerializationToolbox::WriteSetOfStrings(target, failedInstances_, KEY_FAILED_INSTANCES);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+
+  SetOfInstancesJob::SetOfInstancesJob(const Json::Value& source) :
+    SetOfCommandsJob(new InstanceUnserializer(*this), source)
+  {
+    SerializationToolbox::ReadSetOfStrings(failedInstances_, source, KEY_FAILED_INSTANCES);
+
+    if (source.isMember(KEY_TRAILING_STEP))
+    {
+      hasTrailingStep_ = SerializationToolbox::ReadBoolean(source, KEY_TRAILING_STEP);
+    }
+    else
+    {
+      // Backward compatibility with Orthanc <= 1.4.2
+      hasTrailingStep_ = false;
+    }
+  }
+  
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/SetOfInstancesJob.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,97 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IJob.h"
+#include "SetOfCommandsJob.h"
+
+#include <set>
+
+namespace Orthanc
+{
+  class SetOfInstancesJob : public SetOfCommandsJob
+  {
+  private:
+    class InstanceCommand;
+    class TrailingStepCommand;
+    class InstanceUnserializer;
+    
+    bool                   hasTrailingStep_;
+    std::set<std::string>  failedInstances_;
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance) = 0;
+
+    virtual bool HandleTrailingStep() = 0;
+
+    // Hiding this method, use AddInstance() instead
+    using SetOfCommandsJob::AddCommand;
+
+  public:
+    SetOfInstancesJob();
+
+    SetOfInstancesJob(const Json::Value& source);  // Unserialization
+
+    void AddInstance(const std::string& instance);
+
+    void AddTrailingStep(); 
+
+    size_t GetInstancesCount() const;
+    
+    const std::string& GetInstance(size_t index) const;
+
+    bool HasTrailingStep() const
+    {
+      return hasTrailingStep_;
+    }
+
+    const std::set<std::string>& GetFailedInstances() const
+    {
+      return failedInstances_;
+    }
+
+    bool IsFailedInstance(const std::string& instance) const
+    {
+      return failedInstances_.find(instance) != failedInstances_.end();
+    }
+
+    virtual void Start();
+
+    virtual void Reset();
+
+    virtual void GetPublicContent(Json::Value& target);
+
+    virtual bool Serialize(Json::Value& target);
+  };
+}
--- a/Core/Logging.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Logging.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -47,6 +48,14 @@
     {
     }
 
+    void Reset()
+    {
+    }
+
+    void Flush()
+    {
+    }
+
     void EnableInfoLevel(bool enabled)
     {
     }
@@ -55,90 +64,179 @@
     {
     }
 
+    void SetTargetFile(const std::string& path)
+    {
+    }
+
     void SetTargetFolder(const std::string& path)
     {
     }
   }
 }
 
-#elif ORTHANC_ENABLE_GOOGLE_LOG == 1
+
+#elif ORTHANC_ENABLE_LOGGING_PLUGIN == 1
 
 /*********************************************************
- * Wrapper around Google Log
+ * Logger compatible with the Orthanc plugin SDK
  *********************************************************/
 
+#include <boost/lexical_cast.hpp>
+
 namespace Orthanc
-{  
+{
   namespace Logging
   {
-    void Initialize()
+    static OrthancPluginContext* context_ = NULL;
+
+    void Initialize(OrthancPluginContext* context)
     {
-      // Initialize Google's logging library.
-      FLAGS_logtostderr = true;
-      FLAGS_minloglevel = 1;   // Do not print LOG(INFO) by default
-      FLAGS_v = 0;             // Do not print trace-level VLOG(1) by default
+      context_ = context;
+    }
 
-      google::InitGoogleLogging("Orthanc");
+    InternalLogger::InternalLogger(InternalLevel level,
+                                   const char* file  /* ignored */,
+                                   int line  /* ignored */) :
+      level_(level)
+    {
     }
 
-    void Finalize()
+    InternalLogger::~InternalLogger()
     {
-      google::ShutdownGoogleLogging();
+      if (context_ != NULL)
+      {
+        switch (level_)
+        {
+          case InternalLevel_ERROR:
+            OrthancPluginLogError(context_, message_.c_str());
+            break;
+
+          case InternalLevel_WARNING:
+            OrthancPluginLogWarning(context_, message_.c_str());
+            break;
+
+          case InternalLevel_INFO:
+            OrthancPluginLogInfo(context_, message_.c_str());
+            break;
+
+          case InternalLevel_TRACE:
+            // Not used by plugins
+            break;
+
+          default:
+          {
+            std::string s = ("Unknown log level (" + boost::lexical_cast<std::string>(level_) +
+                             ") for message: " + message_);
+            OrthancPluginLogError(context_, s.c_str());
+            break;
+          }
+        }
+      }
+    }
+  }
+}
+
+
+#elif ORTHANC_ENABLE_LOGGING_STDIO == 1
+
+/*********************************************************
+ * Logger compatible with <stdio.h>
+ *********************************************************/
+
+#include <stdio.h>
+#include <boost/lexical_cast.hpp>
+
+namespace Orthanc
+{
+  namespace Logging
+  {
+    static bool globalVerbose_ = false;
+    static bool globalTrace_ = false;
+    
+    InternalLogger::InternalLogger(InternalLevel level,
+                                   const char* file  /* ignored */,
+                                   int line  /* ignored */) :
+      level_(level)
+    {
+    }
+
+    InternalLogger::~InternalLogger()
+    {
+      switch (level_)
+      {
+        case InternalLevel_ERROR:
+          fprintf(stderr, "E: %s\n", message_.c_str());
+          break;
+
+        case InternalLevel_WARNING:
+          fprintf(stdout, "W: %s\n", message_.c_str());
+          break;
+
+        case InternalLevel_INFO:
+          if (globalVerbose_)
+          {
+            fprintf(stdout, "I: %s\n", message_.c_str());
+          }
+          break;
+
+        case InternalLevel_TRACE:
+          if (globalTrace_)
+          {
+            fprintf(stdout, "T: %s\n", message_.c_str());
+          }
+          break;
+
+        default:
+          fprintf(stderr, "Unknown log level (%d) for message: %s\n", level_, message_.c_str());
+      }
     }
 
     void EnableInfoLevel(bool enabled)
     {
-      FLAGS_minloglevel = (enabled ? 0 : 1);
+      globalVerbose_ = enabled;
     }
 
     void EnableTraceLevel(bool enabled)
     {
-      if (enabled)
-      {
-        FLAGS_minloglevel = 0;
-        FLAGS_v = 1;
-      }
-      else
-      {
-        FLAGS_v = 0;
-      }
-    }
-
-    void SetTargetFolder(const std::string& path)
-    {
-      FLAGS_logtostderr = false;
-      FLAGS_log_dir = path;
+      globalTrace_ = enabled;
     }
   }
 }
 
-#else
+
+#else  /* ORTHANC_ENABLE_LOGGING_PLUGIN == 0 && 
+          ORTHANC_ENABLE_LOGGING_STDIO == 0 && 
+          ORTHANC_ENABLE_LOGGING == 1 */
 
 /*********************************************************
- * Use internal logger, not Google Log
+ * Internal logger of Orthanc, that mimics some
+ * behavior from Google Log.
  *********************************************************/
 
 #include "OrthancException.h"
 #include "Enumerations.h"
 #include "Toolbox.h"
 
+#if ORTHANC_SANDBOXED == 1
+#  include <stdio.h>
+#else
+#  include "SystemToolbox.h"
+#endif
+
 #include <fstream>
 #include <boost/filesystem.hpp>
 #include <boost/thread.hpp>
-
-#if BOOST_HAS_DATE_TIME == 1
-#  include <boost/date_time/posix_time/posix_time.hpp>
-#else
-#  error Boost::date_time is required
-#endif
+#include <boost/date_time/posix_time/posix_time.hpp>
 
 
 namespace
 {
-  struct LoggingState
+  struct LoggingContext
   {
     bool infoEnabled_;
     bool traceEnabled_;
+    std::string  targetFile_;
+    std::string  targetFolder_;
 
     std::ostream* error_;
     std::ostream* warning_;
@@ -146,7 +244,7 @@
 
     std::auto_ptr<std::ofstream> file_;
 
-    LoggingState() : 
+    LoggingContext() : 
       infoEnabled_(false),
       traceEnabled_(false),
       error_(&std::cerr),
@@ -159,7 +257,7 @@
 
 
 
-static std::auto_ptr<LoggingState> loggingState_;
+static std::auto_ptr<LoggingContext> loggingContext_;
 static boost::mutex  loggingMutex_;
 
 
@@ -186,7 +284,7 @@
 
       boost::posix_time::ptime now = boost::posix_time::second_clock::local_time();
       boost::filesystem::path root(directory);
-      boost::filesystem::path exe(Toolbox::GetPathToExecutable());
+      boost::filesystem::path exe(SystemToolbox::GetPathToExecutable());
       
       if (!boost::filesystem::exists(root) ||
           !boost::filesystem::is_directory(root))
@@ -199,10 +297,10 @@
               static_cast<int>(now.date().year()),
               now.date().month().as_number(),
               now.date().day().as_number(),
-              now.time_of_day().hours(),
-              now.time_of_day().minutes(),
-              now.time_of_day().seconds(),
-              Toolbox::GetProcessId());
+              static_cast<int>(now.time_of_day().hours()),
+              static_cast<int>(now.time_of_day().minutes()),
+              static_cast<int>(now.time_of_day().seconds()),
+              SystemToolbox::GetProcessId());
 
       std::string programName = exe.filename().replace_extension("").string();
 
@@ -211,9 +309,9 @@
     }
 
 
-    static void PrepareLogFile(std::auto_ptr<std::ofstream>& file,
-                               const std::string& suffix,
-                               const std::string& directory)
+    static void PrepareLogFolder(std::auto_ptr<std::ofstream>& file,
+                                 const std::string& suffix,
+                                 const std::string& directory)
     {
       boost::filesystem::path log, link;
       GetLogPath(log, link, suffix, directory);
@@ -230,168 +328,247 @@
     void Initialize()
     {
       boost::mutex::scoped_lock lock(loggingMutex_);
-      loggingState_.reset(new LoggingState);
+      loggingContext_.reset(new LoggingContext);
     }
 
     void Finalize()
     {
       boost::mutex::scoped_lock lock(loggingMutex_);
-      loggingState_.reset(NULL);
+      loggingContext_.reset(NULL);
+    }
+
+    void Reset()
+    {
+      // Recover the old logging context
+      std::auto_ptr<LoggingContext> old;
+
+      {
+        boost::mutex::scoped_lock lock(loggingMutex_);
+        if (loggingContext_.get() == NULL)
+        {
+          return;
+        }
+        else
+        {
+          old = loggingContext_;
+
+          // Create a new logging context, 
+          loggingContext_.reset(new LoggingContext);
+        }
+      }
+      
+      EnableInfoLevel(old->infoEnabled_);
+      EnableTraceLevel(old->traceEnabled_);
+
+      if (!old->targetFolder_.empty())
+      {
+        SetTargetFolder(old->targetFolder_);
+      }
+      else if (!old->targetFile_.empty())
+      {
+        SetTargetFile(old->targetFile_);
+      }
     }
 
     void EnableInfoLevel(bool enabled)
     {
       boost::mutex::scoped_lock lock(loggingMutex_);
-      assert(loggingState_.get() != NULL);
+      assert(loggingContext_.get() != NULL);
 
-      loggingState_->infoEnabled_ = enabled;
+      loggingContext_->infoEnabled_ = enabled;
+      
+      if (!enabled)
+      {
+        // Also disable the "TRACE" level when info-level debugging is disabled
+        loggingContext_->traceEnabled_ = false;
+      }
     }
 
     void EnableTraceLevel(bool enabled)
     {
       boost::mutex::scoped_lock lock(loggingMutex_);
-      assert(loggingState_.get() != NULL);
+      assert(loggingContext_.get() != NULL);
 
-      loggingState_->traceEnabled_ = enabled;
+      loggingContext_->traceEnabled_ = enabled;
       
       if (enabled)
       {
         // Also enable the "INFO" level when trace-level debugging is enabled
-        loggingState_->infoEnabled_ = true;
+        loggingContext_->infoEnabled_ = true;
+      }
+    }
+
+
+    static void CheckFile(std::auto_ptr<std::ofstream>& f)
+    {
+      if (loggingContext_->file_.get() == NULL ||
+          !loggingContext_->file_->is_open())
+      {
+        throw OrthancException(ErrorCode_CannotWriteFile);
       }
     }
 
     void SetTargetFolder(const std::string& path)
     {
       boost::mutex::scoped_lock lock(loggingMutex_);
-      assert(loggingState_.get() != NULL);
+      assert(loggingContext_.get() != NULL);
+
+      PrepareLogFolder(loggingContext_->file_, "" /* no suffix */, path);
+      CheckFile(loggingContext_->file_);
 
-      PrepareLogFile(loggingState_->file_, "" /* no suffix */, path);
+      loggingContext_->targetFile_.clear();
+      loggingContext_->targetFolder_ = path;
+      loggingContext_->warning_ = loggingContext_->file_.get();
+      loggingContext_->error_ = loggingContext_->file_.get();
+      loggingContext_->info_ = loggingContext_->file_.get();
+    }
+
 
-      loggingState_->warning_ = loggingState_->file_.get();
-      loggingState_->error_ = loggingState_->file_.get();
-      loggingState_->info_ = loggingState_->file_.get();
+    void SetTargetFile(const std::string& path)
+    {
+      boost::mutex::scoped_lock lock(loggingMutex_);
+      assert(loggingContext_.get() != NULL);
+
+      loggingContext_->file_.reset(new std::ofstream(path.c_str(), std::fstream::app));
+      CheckFile(loggingContext_->file_);
+
+      loggingContext_->targetFile_ = path;
+      loggingContext_->targetFolder_.clear();
+      loggingContext_->warning_ = loggingContext_->file_.get();
+      loggingContext_->error_ = loggingContext_->file_.get();
+      loggingContext_->info_ = loggingContext_->file_.get();
     }
 
+
     InternalLogger::InternalLogger(const char* level,
                                    const char* file,
                                    int line) : 
       lock_(loggingMutex_), 
       stream_(&null_)  // By default, logging to "/dev/null" is simulated
     {
-      if (loggingState_.get() == NULL)
+      if (loggingContext_.get() == NULL)
       {
         fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine\n");
         return;
       }
 
-      LogLevel l = StringToLogLevel(level);
-      
-      if ((l == LogLevel_Info  && !loggingState_->infoEnabled_) ||
-          (l == LogLevel_Trace && !loggingState_->traceEnabled_))
+      try
       {
-        // This logging level is disabled, directly exit and unlock
-        // the mutex to speed-up things. The stream is set to "/dev/null"
-        lock_.unlock();
-        return;
-      }
+        LogLevel l = StringToLogLevel(level);
+      
+        if ((l == LogLevel_Info  && !loggingContext_->infoEnabled_) ||
+            (l == LogLevel_Trace && !loggingContext_->traceEnabled_))
+        {
+          // This logging level is disabled, directly exit and unlock
+          // the mutex to speed-up things. The stream is set to "/dev/null"
+          lock_.unlock();
+          return;
+        }
 
-      // Compute the header of the line, temporary release the lock as
-      // this is a time-consuming operation
-      lock_.unlock();
-      std::string header;
+        // Compute the header of the line, temporary release the lock as
+        // this is a time-consuming operation
+        lock_.unlock();
+        std::string header;
 
-      {
-        boost::filesystem::path path(file);
-        boost::posix_time::ptime now = boost::posix_time::microsec_clock::local_time();
-        boost::posix_time::time_duration duration = now.time_of_day();
+        {
+          boost::filesystem::path path(file);
+          boost::posix_time::ptime now = boost::posix_time::microsec_clock::local_time();
+          boost::posix_time::time_duration duration = now.time_of_day();
 
-        /**
-           From Google Log documentation:
+          /**
+             From Google Log documentation:
 
-           "Log lines have this form:
+             "Log lines have this form:
 
-           Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg...
+             Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg...
 
-           where the fields are defined as follows:
+             where the fields are defined as follows:
 
-           L                A single character, representing the log level (eg 'I' for INFO)
-           mm               The month (zero padded; ie May is '05')
-           dd               The day (zero padded)
-           hh:mm:ss.uuuuuu  Time in hours, minutes and fractional seconds
-           threadid         The space-padded thread ID as returned by GetTID() (this matches the PID on Linux)
-           file             The file name
-           line             The line number
-           msg              The user-supplied message"
+             L                A single character, representing the log level (eg 'I' for INFO)
+             mm               The month (zero padded; ie May is '05')
+             dd               The day (zero padded)
+             hh:mm:ss.uuuuuu  Time in hours, minutes and fractional seconds
+             threadid         The space-padded thread ID as returned by GetTID() (this matches the PID on Linux)
+             file             The file name
+             line             The line number
+             msg              The user-supplied message"
 
-           In this implementation, "threadid" is not printed.
-         **/
+             In this implementation, "threadid" is not printed.
+          **/
 
-        char date[32];
-        sprintf(date, "%c%02d%02d %02d:%02d:%02d.%06d ",
-                level[0],
-                now.date().month().as_number(),
-                now.date().day().as_number(),
-                duration.hours(),
-                duration.minutes(),
-                duration.seconds(),
-                static_cast<int>(duration.fractional_seconds()));
+          char date[40];
+          sprintf(date, "%c%02d%02d %02d:%02d:%02d.%06d ",
+                  level[0],
+                  now.date().month().as_number(),
+                  now.date().day().as_number(),
+                  static_cast<int>(duration.hours()),
+                  static_cast<int>(duration.minutes()),
+                  static_cast<int>(duration.seconds()),
+                  static_cast<int>(duration.fractional_seconds()));
 
-        header = std::string(date) + path.filename().string() + ":" + boost::lexical_cast<std::string>(line) + "] ";
-      }
+          header = std::string(date) + path.filename().string() + ":" + boost::lexical_cast<std::string>(line) + "] ";
+        }
 
 
-      // The header is computed, we now re-lock the mutex to access
-      // the stream objects. Pay attention that "loggingState_",
-      // "infoEnabled_" or "traceEnabled_" might have changed while
-      // the mutex was unlocked.
-      lock_.lock();
+        // The header is computed, we now re-lock the mutex to access
+        // the stream objects. Pay attention that "loggingContext_",
+        // "infoEnabled_" or "traceEnabled_" might have changed while
+        // the mutex was unlocked.
+        lock_.lock();
+
+        if (loggingContext_.get() == NULL)
+        {
+          fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine\n");
+          return;
+        }
 
-      if (loggingState_.get() == NULL)
-      {
-        fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine\n");
-        return;
-      }
+        switch (l)
+        {
+          case LogLevel_Error:
+            stream_ = loggingContext_->error_;
+            break;
 
-      switch (l)
-      {
-        case LogLevel_Error:
-          stream_ = loggingState_->error_;
-          break;
+          case LogLevel_Warning:
+            stream_ = loggingContext_->warning_;
+            break;
 
-        case LogLevel_Warning:
-          stream_ = loggingState_->warning_;
-          break;
+          case LogLevel_Info:
+            if (loggingContext_->infoEnabled_)
+            {
+              stream_ = loggingContext_->info_;
+            }
+
+            break;
 
-        case LogLevel_Info:
-          if (loggingState_->infoEnabled_)
-          {
-            stream_ = loggingState_->info_;
-          }
+          case LogLevel_Trace:
+            if (loggingContext_->traceEnabled_)
+            {
+              stream_ = loggingContext_->info_;
+            }
 
-          break;
+            break;
 
-        case LogLevel_Trace:
-          if (loggingState_->traceEnabled_)
-          {
-            stream_ = loggingState_->info_;
-          }
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
 
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
+        if (stream_ == &null_)
+        {
+          // The logging is disabled for this level. The stream is the
+          // "null_" member of this object, so we can release the global
+          // mutex.
+          lock_.unlock();
+        }
 
-      if (stream_ == &null_)
-      {
-        // The logging is disabled for this level. The stream is the
-        // "null_" member of this object, so we can release the global
-        // mutex.
-        lock_.unlock();
+        (*stream_) << header;
       }
-
-      (*stream_) << header;
+      catch (...)
+      { 
+        // Something is going really wrong, probably running out of
+        // memory. Fallback to a degraded mode.
+        stream_ = loggingContext_->error_;
+        (*stream_) << "E???? ??:??:??.?????? ] ";
+      }
     }
 
 
@@ -410,6 +587,16 @@
     }
       
 
+    void Flush()
+    {
+      boost::mutex::scoped_lock lock(loggingMutex_);
+
+      if (loggingContext_.get() != NULL &&
+          loggingContext_->file_.get() != NULL)
+      {
+        loggingContext_->file_->flush();
+      }
+    }
   }
 }
 
--- a/Core/Logging.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Logging.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,18 +35,54 @@
 
 #include <iostream>
 
+#if !defined(ORTHANC_ENABLE_LOGGING)
+#  error The macro ORTHANC_ENABLE_LOGGING must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_LOGGING_PLUGIN)
+#  if ORTHANC_ENABLE_LOGGING == 1
+#    error The macro ORTHANC_ENABLE_LOGGING_PLUGIN must be defined
+#  else
+#    define ORTHANC_ENABLE_LOGGING_PLUGIN 0
+#  endif
+#endif
+
+#if !defined(ORTHANC_ENABLE_LOGGING_STDIO)
+#  if ORTHANC_ENABLE_LOGGING == 1
+#    error The macro ORTHANC_ENABLE_LOGGING_STDIO must be defined
+#  else
+#    define ORTHANC_ENABLE_LOGGING_STDIO 0
+#  endif
+#endif
+
+#if ORTHANC_ENABLE_LOGGING_PLUGIN == 1
+#  include <orthanc/OrthancCPlugin.h>
+#endif
+
+#include <boost/lexical_cast.hpp>
+
 namespace Orthanc
 {
   namespace Logging
   {
+#if ORTHANC_ENABLE_LOGGING_PLUGIN == 1
+    void Initialize(OrthancPluginContext* context);
+#else
     void Initialize();
+#endif
 
     void Finalize();
 
+    void Reset();
+
+    void Flush();
+
     void EnableInfoLevel(bool enabled);
 
     void EnableTraceLevel(bool enabled);
 
+    void SetTargetFile(const std::string& path);
+
     void SetTargetFolder(const std::string& path);
 
     struct NullStream : public std::ostream 
@@ -56,7 +93,8 @@
       {
       }
       
-      std::ostream& operator<< (const std::string& message)
+      template <typename T>
+      std::ostream& operator<< (const T& message)
       {
         return *this;
       }
@@ -70,18 +108,62 @@
 #  define LOG(level)   ::Orthanc::Logging::NullStream()
 #  define VLOG(level)  ::Orthanc::Logging::NullStream()
 
-#else  /* ORTHANC_ENABLE_LOGGING == 1 */
+
+#elif (ORTHANC_ENABLE_LOGGING_PLUGIN == 1 ||    \
+       ORTHANC_ENABLE_LOGGING_STDIO == 1)
+
+#  include <boost/noncopyable.hpp>
+#  define LOG(level)  ::Orthanc::Logging::InternalLogger \
+  (::Orthanc::Logging::InternalLevel_ ## level, __FILE__, __LINE__)
+#  define VLOG(level) ::Orthanc::Logging::InternalLogger \
+  (::Orthanc::Logging::InternalLevel_TRACE, __FILE__, __LINE__)
 
-#if ORTHANC_ENABLE_GOOGLE_LOG == 1
-#  include <stdlib.h>  // Including this fixes a problem in glog for recent releases of MinGW
-#  include <glog/logging.h>
-#else
+namespace Orthanc
+{
+  namespace Logging
+  {
+    enum InternalLevel
+    {
+      InternalLevel_ERROR,
+      InternalLevel_WARNING,
+      InternalLevel_INFO,
+      InternalLevel_TRACE
+    };
+    
+    class InternalLogger : public boost::noncopyable
+    {
+    private:
+      InternalLevel  level_;
+      std::string    message_;
+
+    public:
+      InternalLogger(InternalLevel level,
+                     const char* file,
+                     int line);
+
+      ~InternalLogger();
+      
+      template <typename T>
+      InternalLogger& operator<< (const T& message)
+      {
+        message_ += boost::lexical_cast<std::string>(message);
+        return *this;
+      }
+    };
+  }
+}
+
+
+
+
+#else  /* ORTHANC_ENABLE_LOGGING_PLUGIN == 0 && 
+          ORTHANC_ENABLE_LOGGING_STDIO == 0 && 
+          ORTHANC_ENABLE_LOGGING == 1 */
+
 #  include <boost/thread/mutex.hpp>
 #  define LOG(level)  ::Orthanc::Logging::InternalLogger(#level,  __FILE__, __LINE__)
 #  define VLOG(level) ::Orthanc::Logging::InternalLogger("TRACE", __FILE__, __LINE__)
-#endif
 
-#if ORTHANC_ENABLE_GOOGLE_LOG != 1
 namespace Orthanc
 {
   namespace Logging
@@ -100,13 +182,13 @@
 
       ~InternalLogger();
       
-      std::ostream& operator<< (const std::string& message)
+      template <typename T>
+      std::ostream& operator<< (const T& message)
       {
-        return (*stream_) << message;
+        return (*stream_) << boost::lexical_cast<std::string>(message);
       }
     };
   }
 }
-#endif
 
 #endif  // ORTHANC_ENABLE_LOGGING
--- a/Core/Lua/LuaContext.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Lua/LuaContext.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -61,7 +62,6 @@
     return true;
   }
   
-
   LuaContext& LuaContext::GetLuaContext(lua_State *state)
   {
     const void* value = GetGlobalVariable(state, "_LuaContext");
@@ -209,14 +209,36 @@
     return true;
   }
 
+  void LuaContext::SetHttpHeaders(lua_State *state, int top)
+  {
+    this->httpClient_.ClearHeaders(); // always reset headers in case they have been set in a previous request
+
+    if (lua_gettop(state) >= top)
+    {
+      Json::Value headers;
+      this->GetJson(headers, top, true);
+
+      Json::Value::Members members = headers.getMemberNames();
+
+      for (Json::Value::Members::const_iterator 
+           it = members.begin(); it != members.end(); ++it)
+      {
+        this->httpClient_.AddHeader(*it, headers[*it].asString());
+      }
+    }
+
+  }
   
+
+
   int LuaContext::CallHttpGet(lua_State *state)
   {
     LuaContext& that = GetLuaContext(state);
 
     // Check the types of the arguments
     int nArgs = lua_gettop(state);
-    if (nArgs != 1 || !lua_isstring(state, 1))  // URL
+    if ((nArgs < 1 || nArgs > 2) ||         // check args count
+       !lua_isstring(state, 1))             // URL is a string
     {
       LOG(ERROR) << "Lua: Bad parameters to HttpGet()";
       lua_pushnil(state);
@@ -227,6 +249,8 @@
     const char* url = lua_tostring(state, 1);
     that.httpClient_.SetMethod(HttpMethod_Get);
     that.httpClient_.SetUrl(url);
+    that.httpClient_.GetBody().clear();
+    that.SetHttpHeaders(state, 2);
 
     // Do the HTTP GET request
     if (!that.AnswerHttpQuery(state))
@@ -246,9 +270,9 @@
 
     // Check the types of the arguments
     int nArgs = lua_gettop(state);
-    if ((nArgs != 1 && nArgs != 2) ||
-        !lua_isstring(state, 1) ||                // URL
-        (nArgs >= 2 && !lua_isstring(state, 2)))  // Body data
+    if ((nArgs < 1 || nArgs > 3) ||                 // check arg count
+        !lua_isstring(state, 1) ||                  // URL is a string
+        (nArgs >= 2 && (!lua_isstring(state, 2) && !lua_isnil(state, 2))))    // Body data is null or is a string
     {
       LOG(ERROR) << "Lua: Bad parameters to HttpPost() or HttpPut()";
       lua_pushnil(state);
@@ -259,10 +283,21 @@
     const char* url = lua_tostring(state, 1);
     that.httpClient_.SetMethod(method);
     that.httpClient_.SetUrl(url);
+    that.SetHttpHeaders(state, 3);
 
-    if (nArgs >= 2)
+    if (nArgs >= 2 && !lua_isnil(state, 2))
     {
-      that.httpClient_.SetBody(lua_tostring(state, 2));
+      size_t bodySize = 0;
+      const char* bodyData = lua_tolstring(state, 2, &bodySize);
+
+      if (bodySize == 0)
+      {
+        that.httpClient_.GetBody().clear();
+      }
+      else
+      {
+        that.httpClient_.GetBody().assign(bodyData, bodySize);
+      }
     }
     else
     {
@@ -298,7 +333,7 @@
 
     // Check the types of the arguments
     int nArgs = lua_gettop(state);
-    if (nArgs != 1 || !lua_isstring(state, 1))  // URL
+    if (nArgs < 1 || nArgs > 2 || !lua_isstring(state, 1))  // URL
     {
       LOG(ERROR) << "Lua: Bad parameters to HttpDelete()";
       lua_pushnil(state);
@@ -309,6 +344,8 @@
     const char* url = lua_tostring(state, 1);
     that.httpClient_.SetMethod(HttpMethod_Delete);
     that.httpClient_.SetUrl(url);
+    that.httpClient_.GetBody().clear();
+    that.SetHttpHeaders(state, 2);
 
     // Do the HTTP DELETE request
     std::string s;
@@ -497,6 +534,10 @@
       // Lua can convert most types to strings by default.
       result = std::string(lua_tostring(lua_, top));
     }
+    else if (lua_isboolean(lua_, top))
+    {
+      result = lua_toboolean(lua_, top) ? true : false;
+    }
     else
     {
       LOG(WARNING) << "Unsupported Lua type when returning Json";
@@ -557,12 +598,14 @@
   }
 
 
+#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1
   void LuaContext::Execute(EmbeddedResources::FileResourceId resource)
   {
     std::string command;
     EmbeddedResources::GetFileResource(command, resource);
     ExecuteInternal(NULL, command);
   }
+#endif
 
 
   bool LuaContext::IsExistingFunction(const char* name)
--- a/Core/Lua/LuaContext.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Lua/LuaContext.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,6 +33,22 @@
 
 #pragma once
 
+#if !defined(ORTHANC_ENABLE_LUA)
+#  error The macro ORTHANC_ENABLE_LUA must be defined
+#endif
+
+#if !defined(ORTHANC_HAS_EMBEDDED_RESOURCES)
+#  error Macro ORTHANC_HAS_EMBEDDED_RESOURCES must be defined
+#endif
+
+#if ORTHANC_ENABLE_LUA == 0
+#  error The Lua support is disabled, cannot include this file
+#endif
+
+#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1
+#  include <EmbeddedResources.h>   // Autogenerated file
+#endif
+
 #include "../HttpClient.h"
 
 extern "C" 
@@ -39,7 +56,6 @@
 #include <lua.h>
 }
 
-#include <EmbeddedResources.h>
 #include <boost/noncopyable.hpp>
 
 namespace Orthanc
@@ -74,6 +90,8 @@
     void GetJson(Json::Value& result,
                  int top,
                  bool keepStrings);
+
+    void SetHttpHeaders(lua_State* state, int top);
     
   public:
     LuaContext();
@@ -94,7 +112,9 @@
     void Execute(Json::Value& output,
                  const std::string& command);
 
+#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1
     void Execute(EmbeddedResources::FileResourceId resource);
+#endif
 
     bool IsExistingFunction(const char* name);
 
@@ -104,11 +124,6 @@
       httpClient_.SetCredentials(username, password);
     }
 
-    void SetHttpProxy(const std::string& proxy)
-    {
-      httpClient_.SetProxy(proxy);
-    }
-
     void RegisterFunction(const char* name,
                           lua_CFunction func);
 
--- a/Core/Lua/LuaFunctionCall.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Lua/LuaFunctionCall.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -152,4 +153,40 @@
       throw OrthancException(ErrorCode_LuaReturnsNoString);
     }
   }
+
+
+  void LuaFunctionCall::PushStringMap(const std::map<std::string, std::string>& value)
+  {
+    Json::Value json = Json::objectValue;
+
+    for (std::map<std::string, std::string>::const_iterator
+           it = value.begin(); it != value.end(); ++it)
+    {
+      json[it->first] = it->second;
+    }
+
+    PushJson(json);
+  }
+
+
+  void LuaFunctionCall::PushDicom(const DicomMap& dicom)
+  {
+    DicomArray a(dicom);
+    PushDicom(a);
+  }
+
+
+  void LuaFunctionCall::PushDicom(const DicomArray& dicom)
+  {
+    Json::Value value = Json::objectValue;
+
+    for (size_t i = 0; i < dicom.GetSize(); i++)
+    {
+      const DicomValue& v = dicom.GetElement(i).GetValue();
+      std::string s = (v.IsNull() || v.IsBinary()) ? "" : v.GetContent();
+      value[dicom.GetElement(i).GetTag().Format()] = s;
+    }
+
+    PushJson(value);
+  }
 }
--- a/Core/Lua/LuaFunctionCall.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Lua/LuaFunctionCall.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,6 +35,9 @@
 
 #include "LuaContext.h"
 
+#include "../DicomFormat/DicomArray.h"
+#include "../DicomFormat/DicomMap.h"
+
 #include <json/json.h>
 
 namespace Orthanc
@@ -46,8 +50,14 @@
 
     void CheckAlreadyExecuted();
 
+  protected:
     void ExecuteInternal(int numOutputs);
 
+    lua_State* GetState()
+    {
+      return context_.lua_;
+    }
+
   public:
     LuaFunctionCall(LuaContext& context,
                     const char* functionName);
@@ -62,6 +72,12 @@
 
     void PushJson(const Json::Value& value);
 
+    void PushStringMap(const std::map<std::string, std::string>& value);
+
+    void PushDicom(const DicomMap& dicom);
+
+    void PushDicom(const DicomArray& dicom);
+
     void Execute()
     {
       ExecuteInternal(0);
--- a/Core/MultiThreading/ILockable.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class ILockable : public boost::noncopyable
-  {
-    friend class Locker;
-
-  protected:
-    virtual void Lock() = 0;
-
-    virtual void Unlock() = 0;
-
-  public:
-    virtual ~ILockable()
-    {
-    }
-  };
-}
--- a/Core/MultiThreading/IRunnableBySteps.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/MultiThreading/IRunnableBySteps.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/MultiThreading/Locker.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ILockable.h"
-
-namespace Orthanc
-{
-  class Locker : public boost::noncopyable
-  {
-  private:
-    ILockable& lockable_;
-
-  public:
-    Locker(ILockable& lockable) : lockable_(lockable)
-    {
-      lockable_.Lock();
-    }
-
-    virtual ~Locker()
-    {
-      lockable_.Unlock();
-    }
-  };
-}
--- a/Core/MultiThreading/Mutex.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,121 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeaders.h"
-#include "Mutex.h"
-
-#include "../OrthancException.h"
-
-#if defined(_WIN32)
-#include <windows.h>
-#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__)
-#include <pthread.h>
-#else
-#error Support your platform here
-#endif
-
-namespace Orthanc
-{
-#if defined (_WIN32)
-
-  struct Mutex::PImpl
-  {
-    CRITICAL_SECTION criticalSection_;
-  };
-
-  Mutex::Mutex()
-  {
-    pimpl_ = new PImpl;
-    ::InitializeCriticalSection(&pimpl_->criticalSection_);
-  }
-
-  Mutex::~Mutex()
-  {
-    ::DeleteCriticalSection(&pimpl_->criticalSection_);
-    delete pimpl_;
-  }
-
-  void Mutex::Lock()
-  {
-    ::EnterCriticalSection(&pimpl_->criticalSection_);
-  }
-
-  void Mutex::Unlock()
-  {
-    ::LeaveCriticalSection(&pimpl_->criticalSection_);
-  }
-
-
-#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__)
-
-  struct Mutex::PImpl
-  {
-    pthread_mutex_t mutex_;
-  };
-
-  Mutex::Mutex()
-  {
-    pimpl_ = new PImpl;
-
-    if (pthread_mutex_init(&pimpl_->mutex_, NULL) != 0)
-    {
-      delete pimpl_;
-      throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-  Mutex::~Mutex()
-  {
-    pthread_mutex_destroy(&pimpl_->mutex_);
-    delete pimpl_;
-  }
-
-  void Mutex::Lock()
-  {
-    if (pthread_mutex_lock(&pimpl_->mutex_) != 0)
-    {
-      throw OrthancException(ErrorCode_InternalError);    
-    }
-  }
-
-  void Mutex::Unlock()
-  {
-    if (pthread_mutex_unlock(&pimpl_->mutex_) != 0)
-    {
-      throw OrthancException(ErrorCode_InternalError);    
-    }
-  }
-
-#else
-#error Support your plateform here
-#endif
-}
--- a/Core/MultiThreading/Mutex.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ILockable.h"
-
-namespace Orthanc
-{
-  class Mutex : public ILockable
-  {
-  private:
-    struct PImpl;
-
-    PImpl *pimpl_;
-
-  protected:
-    virtual void Lock();
-
-    virtual void Unlock();
-    
-  public:
-    Mutex();
-
-    ~Mutex();
-  };
-}
--- a/Core/MultiThreading/ReaderWriterLock.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeaders.h"
-#include "ReaderWriterLock.h"
-
-#include <boost/thread/shared_mutex.hpp>
-
-namespace Orthanc
-{
-  namespace
-  {
-    // Anonymous namespace to avoid clashes between compilation
-    // modules.
-
-    class ReaderLockable : public ILockable
-    {
-    private:
-      boost::shared_mutex& lock_;
-
-    protected:
-      virtual void Lock()
-      {
-        lock_.lock_shared();
-      }
-
-      virtual void Unlock()
-      {
-        lock_.unlock_shared();        
-      }
-
-    public:
-      ReaderLockable(boost::shared_mutex& lock) : lock_(lock)
-      {
-      }
-    };
-
-
-    class WriterLockable : public ILockable
-    {
-    private:
-      boost::shared_mutex& lock_;
-
-    protected:
-      virtual void Lock()
-      {
-        lock_.lock();
-      }
-
-      virtual void Unlock()
-      {
-        lock_.unlock();        
-      }
-
-    public:
-      WriterLockable(boost::shared_mutex& lock) : lock_(lock)
-      {
-      }
-
-    };
-  }
-
-  struct ReaderWriterLock::PImpl
-  {
-    boost::shared_mutex lock_;
-    ReaderLockable reader_;
-    WriterLockable writer_;
-
-    PImpl() : reader_(lock_), writer_(lock_)
-    {
-    }
-  };
-
-
-  ReaderWriterLock::ReaderWriterLock()
-  {
-    pimpl_ = new PImpl;
-  }
-
-
-  ReaderWriterLock::~ReaderWriterLock()
-  {
-    delete pimpl_;
-  }
-
-
-  ILockable&  ReaderWriterLock::ForReader()
-  {
-    return pimpl_->reader_;
-  }
-
-
-  ILockable&  ReaderWriterLock::ForWriter()
-  {
-    return pimpl_->writer_;
-  }
-}
--- a/Core/MultiThreading/ReaderWriterLock.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ILockable.h"
-
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class ReaderWriterLock : public boost::noncopyable
-  {
-  private:
-    struct PImpl;
-
-    PImpl *pimpl_;
-
-  public:
-    ReaderWriterLock();
-
-    virtual ~ReaderWriterLock();
-
-    ILockable& ForReader();
-
-    ILockable& ForWriter();
-  };
-}
--- a/Core/MultiThreading/RunnableWorkersPool.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/MultiThreading/RunnableWorkersPool.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -52,25 +53,37 @@
       {
         while (that->continue_)
         {
-          std::auto_ptr<IDynamicObject>  obj(that->queue_.Dequeue(100));
-          if (obj.get() != NULL)
+          try
           {
-            try
+            std::auto_ptr<IDynamicObject>  obj(that->queue_.Dequeue(100));
+            if (obj.get() != NULL)
             {
               IRunnableBySteps& runnable = *dynamic_cast<IRunnableBySteps*>(obj.get());
-            
+              
               bool wishToContinue = runnable.Step();
-
+              
               if (wishToContinue)
               {
                 // The runnable wishes to continue, reinsert it at the beginning of the queue
                 that->queue_.Enqueue(obj.release());
               }
             }
-            catch (OrthancException& e)
-            {
-              LOG(ERROR) << "Exception in a pool of working threads: " << e.What();
-            }
+          }
+          catch (OrthancException& e)
+          {
+            LOG(ERROR) << "Exception while handling some runnable object: " << e.What();
+          }
+          catch (std::bad_alloc&)
+          {
+            LOG(ERROR) << "Not enough memory to handle some runnable object";
+          }
+          catch (std::exception& e)
+          {
+            LOG(ERROR) << "std::exception while handling some runnable object: " << e.what();
+          }
+          catch (...)
+          {
+            LOG(ERROR) << "Native exception while handling some runnable object";
           }
         }
       }
@@ -105,7 +118,7 @@
   {
     pimpl_->continue_ = true;
 
-    if (countWorkers <= 0)
+    if (countWorkers == 0)
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
--- a/Core/MultiThreading/RunnableWorkersPool.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/MultiThreading/RunnableWorkersPool.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -47,7 +48,7 @@
     void Stop();
 
   public:
-    RunnableWorkersPool(size_t countWorkers);
+    explicit RunnableWorkersPool(size_t countWorkers);
 
     ~RunnableWorkersPool();
 
--- a/Core/MultiThreading/Semaphore.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/MultiThreading/Semaphore.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -38,9 +39,10 @@
 
 namespace Orthanc
 {
-  Semaphore::Semaphore(unsigned int count) : count_(count)
+  Semaphore::Semaphore(unsigned int count) :
+    count_(count)
   {
-    if (count == 0)
+    if (count_ == 0)
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
--- a/Core/MultiThreading/Semaphore.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/MultiThreading/Semaphore.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -43,12 +44,30 @@
     unsigned int count_;
     boost::mutex mutex_;
     boost::condition_variable condition_;
+    
+    void Release();
+
+    void Acquire();
 
   public:
     explicit Semaphore(unsigned int count);
 
-    void Release();
+    class Locker : public boost::noncopyable
+    {
+    private:
+      Semaphore&  that_;
 
-    void Acquire();
+    public:
+      explicit Locker(Semaphore& that) :
+        that_(that)
+      {
+        that_.Acquire();
+      }
+
+      ~Locker()
+      {
+        that_.Release();
+      }
+    };
   };
 }
--- a/Core/MultiThreading/SharedMessageQueue.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/MultiThreading/SharedMessageQueue.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -185,4 +186,24 @@
     boost::mutex::scoped_lock lock(mutex_);
     isFifo_ = false;
   }
+
+  void SharedMessageQueue::Clear()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (queue_.empty())
+    {
+      return;
+    }
+    else
+    {
+      while (!queue_.empty())
+      {
+        std::auto_ptr<IDynamicObject> message(queue_.front());
+        queue_.pop_front();
+      }
+
+      emptied_.notify_all();
+    }
+  }
 }
--- a/Core/MultiThreading/SharedMessageQueue.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/MultiThreading/SharedMessageQueue.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -78,5 +79,7 @@
     void SetFifoPolicy();
 
     void SetLifoPolicy();
+
+    void Clear();
   };
 }
--- a/Core/OrthancException.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/OrthancException.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -45,7 +46,7 @@
     HttpStatus httpStatus_;
 
   public:
-    OrthancException(ErrorCode errorCode) : 
+    explicit OrthancException(ErrorCode errorCode) : 
       errorCode_(errorCode),
       httpStatus_(ConvertErrorCodeToHttpStatus(errorCode))
     {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Pkcs11.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,309 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeaders.h"
+#include "Pkcs11.h"
+
+
+#if defined(OPENSSL_NO_RSA) || defined(OPENSSL_NO_EC) || defined(OPENSSL_NO_ECDSA) || defined(OPENSSL_NO_ECDH)
+#  error OpenSSL was compiled without support for RSA, EC, ECDSA or ECDH
+#endif
+
+
+#include "Logging.h"
+#include "OrthancException.h"
+#include "SystemToolbox.h"
+
+extern "C"
+{
+#  include <libp11/engine.h>  // This is P11's "engine.h"
+#  include <libp11/libp11.h>
+}
+
+#include <openssl/engine.h>
+
+
+namespace Orthanc
+{
+  namespace Pkcs11
+  {
+    static const char* PKCS11_ENGINE_ID = "pkcs11";
+    static const char* PKCS11_ENGINE_NAME = "PKCS#11 for Orthanc";
+    static const ENGINE_CMD_DEFN PKCS11_ENGINE_COMMANDS[] = 
+    {
+      { 
+        CMD_MODULE_PATH,
+        "MODULE_PATH",
+        "Specifies the path to the PKCS#11 module shared library",
+        ENGINE_CMD_FLAG_STRING
+      },
+      {
+        CMD_PIN,
+        "PIN",
+        "Specifies the pin code",
+        ENGINE_CMD_FLAG_STRING 
+      },
+      {
+        CMD_VERBOSE,
+        "VERBOSE",
+        "Print additional details",
+        ENGINE_CMD_FLAG_NO_INPUT 
+      },
+      {
+        CMD_LOAD_CERT_CTRL,
+        "LOAD_CERT_CTRL",
+        "Get the certificate from card",
+        ENGINE_CMD_FLAG_INTERNAL
+      },
+      {
+        0,
+        NULL, 
+        NULL, 
+        0
+      }
+    };
+
+
+    static bool pkcs11Initialized_ = false;
+    static ENGINE_CTX *context_ = NULL;
+
+    static int EngineInitialize(ENGINE* engine)
+    {
+      if (context_ == NULL)
+      {
+        return 0;
+      }
+      else
+      {
+        return pkcs11_init(context_);
+      }
+    }
+
+
+    static int EngineFinalize(ENGINE* engine)
+    {
+      if (context_ == NULL)
+      {
+        return 0;
+      }
+      else
+      {
+        return pkcs11_finish(context_);
+      }
+    }
+
+
+    static int EngineDestroy(ENGINE* engine)
+    {
+      return (context_ == NULL ? 0 : 1);
+    }
+
+
+    static int EngineControl(ENGINE *engine, 
+                             int command, 
+                             long i, 
+                             void *p, 
+                             void (*f) ())
+    {
+      if (context_ == NULL)
+      {
+        return 0;
+      }
+      else
+      {
+        return pkcs11_engine_ctrl(context_, command, i, p, f);
+      }
+    }
+
+
+    static EVP_PKEY *EngineLoadPublicKey(ENGINE *engine, 
+                                         const char *s_key_id,
+                                         UI_METHOD *ui_method, 
+                                         void *callback_data)
+    {
+      if (context_ == NULL)
+      {
+        return 0;
+      }
+      else
+      {
+        return pkcs11_load_public_key(context_, s_key_id, ui_method, callback_data);
+      }
+    }
+
+
+    static EVP_PKEY *EngineLoadPrivateKey(ENGINE *engine, 
+                                          const char *s_key_id,
+                                          UI_METHOD *ui_method, 
+                                          void *callback_data)
+    {
+      if (context_ == NULL)
+      {
+        return 0;
+      }
+      else
+      {
+        return pkcs11_load_private_key(context_, s_key_id, ui_method, callback_data);
+      }
+    }
+
+
+    static ENGINE* LoadEngine()
+    {
+      // This function creates an engine for PKCS#11 and inspired by
+      // the "ENGINE_load_dynamic" function from OpenSSL, in file
+      // "crypto/engine/eng_dyn.c"
+
+      ENGINE* engine = ENGINE_new();
+      if (!engine)
+      {
+        LOG(ERROR) << "Cannot create an OpenSSL engine for PKCS#11";
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      // Create a PKCS#11 context using libp11
+      context_ = pkcs11_new();
+      if (!context_)
+      {
+        LOG(ERROR) << "Cannot create a libp11 context for PKCS#11";
+        ENGINE_free(engine);
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (!ENGINE_set_id(engine, PKCS11_ENGINE_ID) ||
+          !ENGINE_set_name(engine, PKCS11_ENGINE_NAME) ||
+          !ENGINE_set_cmd_defns(engine, PKCS11_ENGINE_COMMANDS) ||
+
+          // Register the callback functions
+          !ENGINE_set_init_function(engine, EngineInitialize) ||
+          !ENGINE_set_finish_function(engine, EngineFinalize) ||
+          !ENGINE_set_destroy_function(engine, EngineDestroy) ||
+          !ENGINE_set_ctrl_function(engine, EngineControl) ||
+          !ENGINE_set_load_pubkey_function(engine, EngineLoadPublicKey) ||
+          !ENGINE_set_load_privkey_function(engine, EngineLoadPrivateKey) ||
+
+          !ENGINE_set_RSA(engine, PKCS11_get_rsa_method()) ||
+          !ENGINE_set_ECDSA(engine, PKCS11_get_ecdsa_method()) ||
+          !ENGINE_set_ECDH(engine, PKCS11_get_ecdh_method()) ||
+
+#if OPENSSL_VERSION_NUMBER  >= 0x10100002L
+          !ENGINE_set_EC(engine, PKCS11_get_ec_key_method()) ||
+#endif
+
+          // Make OpenSSL know about our PKCS#11 engine
+          !ENGINE_add(engine))
+      {
+        LOG(ERROR) << "Cannot initialize the OpenSSL engine for PKCS#11";
+        pkcs11_finish(context_);
+        ENGINE_free(engine);
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      // If the "ENGINE_add" worked, it gets a structural
+      // reference. We release our just-created reference.
+      ENGINE_free(engine);
+
+      return ENGINE_by_id(PKCS11_ENGINE_ID);
+    }
+
+
+    bool IsInitialized()
+    {
+      return pkcs11Initialized_;
+    }
+
+    const char* GetEngineIdentifier()
+    {
+      return PKCS11_ENGINE_ID;
+    }
+
+    void Initialize(const std::string& module,
+                    const std::string& pin,
+                    bool verbose)
+    {
+      if (pkcs11Initialized_)
+      {
+        LOG(ERROR) << "The PKCS#11 engine has already been initialized";
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      if (module.empty() ||
+          !SystemToolbox::IsRegularFile(module))
+      {
+        LOG(ERROR) << "The PKCS#11 module must be a path to one shared library (DLL or .so)";
+        throw OrthancException(ErrorCode_InexistentFile);
+      }
+
+      ENGINE* engine = LoadEngine();
+      if (!engine)
+      {
+        LOG(ERROR) << "Cannot create an OpenSSL engine for PKCS#11";
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (!ENGINE_ctrl_cmd_string(engine, "MODULE_PATH", module.c_str(), 0))
+      {
+        LOG(ERROR) << "Cannot configure the OpenSSL dynamic engine for PKCS#11";
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (verbose)
+      {
+        ENGINE_ctrl_cmd_string(engine, "VERBOSE", NULL, 0);
+      }
+
+      if (!pin.empty() &&
+          !ENGINE_ctrl_cmd_string(engine, "PIN", pin.c_str(), 0)) 
+      {
+        LOG(ERROR) << "Cannot set the PIN code for PKCS#11";
+        throw OrthancException(ErrorCode_InternalError);
+      }
+  
+      if (!ENGINE_init(engine))
+      {
+        LOG(ERROR) << "Cannot initialize the OpenSSL dynamic engine for PKCS#11";
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      LOG(WARNING) << "The PKCS#11 engine has been successfully initialized";
+      pkcs11Initialized_ = true;
+    }
+
+
+    void Finalize()
+    {
+      // Nothing to do, the unregistration of the engine is
+      // automatically done by OpenSSL
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Pkcs11.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_PKCS11)
+#  error The macro ORTHANC_ENABLE_PKCS11 must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_SSL)
+#  error The macro ORTHANC_ENABLE_SSL must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error This file cannot be used in sandboxed environments
+#endif
+
+#if ORTHANC_ENABLE_PKCS11 != 1 || ORTHANC_ENABLE_SSL != 1
+#  error This file cannot be used if OpenSSL or PKCS#11 support is disabled
+#endif
+
+
+#include <string>
+
+namespace Orthanc
+{
+  namespace Pkcs11
+  {
+    const char* GetEngineIdentifier();
+
+    bool IsInitialized();
+
+    void Initialize(const std::string& module,
+                    const std::string& pin,
+                    bool verbose);
+
+    void Finalize();
+  }
+}
--- a/Core/PrecompiledHeaders.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/PrecompiledHeaders.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/PrecompiledHeaders.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/PrecompiledHeaders.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -48,14 +49,28 @@
 
 #include <json/value.h>
 
-#if ORTHANC_PUGIXML_ENABLED == 1
-#include <pugixml.hpp>
+#if ORTHANC_ENABLE_PUGIXML == 1
+#  include <pugixml.hpp>
 #endif
 
 #include "Enumerations.h"
 #include "Logging.h"
 #include "OrthancException.h"
 #include "Toolbox.h"
-#include "Uuid.h"
+
+#if ORTHANC_ENABLE_DCMTK == 1
+// Headers from DCMTK used in Orthanc headers 
+#  include <dcmtk/dcmdata/dcdatset.h>
+#  include <dcmtk/dcmdata/dcfilefo.h>
+#  include <dcmtk/dcmdata/dcmetinf.h>
+#  include <dcmtk/dcmdata/dcpixseq.h>
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING == 1
+#  include "DicomNetworking/DicomServer.h"
+
+// Headers from DCMTK used in Orthanc headers 
+#  include <dcmtk/dcmnet/dimse.h>
+#endif
 
 #endif
--- a/Core/RestApi/RestApi.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApi.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -185,7 +186,7 @@
   {
     RestApiOutput wrappedOutput(output, method);
 
-#if ORTHANC_PUGIXML_ENABLED == 1
+#if ORTHANC_ENABLE_PUGIXML == 1
     {
       // Look if the client wishes XML answers instead of JSON
       // http://www.w3.org/Protocols/HTTP/HTRQ_Headers.html#z3
--- a/Core/RestApi/RestApi.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApi.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/RestApi/RestApiCall.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApiCall.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/RestApi/RestApiCall.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApiCall.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/RestApi/RestApiDeleteCall.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApiDeleteCall.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/RestApi/RestApiGetCall.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApiGetCall.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/RestApi/RestApiGetCall.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApiGetCall.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/RestApi/RestApiHierarchy.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApiHierarchy.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/RestApi/RestApiHierarchy.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApiHierarchy.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/RestApi/RestApiOutput.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApiOutput.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -35,6 +36,7 @@
 
 #include "../Logging.h"
 #include "../OrthancException.h"
+#include "../Toolbox.h"
 
 #include <boost/lexical_cast.hpp>
 
@@ -91,10 +93,10 @@
 
     if (convertJsonToXml_)
     {
-#if ORTHANC_PUGIXML_ENABLED == 1
+#if ORTHANC_ENABLE_PUGIXML == 1
       std::string s;
       Toolbox::JsonToXml(s, value);
-      output_.SetContentType("application/xml");
+      output_.SetContentType("application/xml; charset=utf-8");
       output_.Answer(s);
 #else
       LOG(ERROR) << "Orthanc was compiled without XML support";
@@ -104,7 +106,7 @@
     else
     {
       Json::StyledWriter writer;
-      output_.SetContentType("application/json");
+      output_.SetContentType("application/json; charset=utf-8");
       output_.Answer(writer.write(value));
     }
 
--- a/Core/RestApi/RestApiOutput.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApiOutput.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/RestApi/RestApiPath.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApiPath.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/RestApi/RestApiPath.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApiPath.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/RestApi/RestApiPostCall.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApiPostCall.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/RestApi/RestApiPutCall.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/RestApi/RestApiPutCall.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/SQLite/Connection.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/Connection.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
@@ -163,7 +163,8 @@
       if (error == SQLITE_ERROR)
       {
 #if ORTHANC_SQLITE_STANDALONE != 1
-        LOG(ERROR) << "SQLite execute error: " << sqlite3_errmsg(db_);
+        LOG(ERROR) << "SQLite execute error: " << sqlite3_errmsg(db_)
+                   << " (" << sqlite3_extended_errcode(db_) << ")";
 #endif
 
         throw OrthancSQLiteException(ErrorCode_SQLiteExecute);
--- a/Core/SQLite/Connection.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/Connection.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
@@ -39,13 +39,11 @@
 
 #include "Statement.h"
 #include "IScalarFunction.h"
+#include "SQLiteTypes.h"
 
 #include <string>
 #include <map>
 
-struct sqlite3;
-struct sqlite3_stmt;
-
 #define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__FILE__, __LINE__)
 
 namespace Orthanc
--- a/Core/SQLite/FunctionContext.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/FunctionContext.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Redistribution and use in source and binary forms, with or without
@@ -49,7 +49,7 @@
   {
     FunctionContext::FunctionContext(struct sqlite3_context* context,
                                      int argc,
-                                     struct ::Mem** argv)
+                                     Internals::SQLiteValue** argv)
     {
       assert(context != NULL);
       assert(argc >= 0);
--- a/Core/SQLite/FunctionContext.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/FunctionContext.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Redistribution and use in source and binary forms, with or without
@@ -36,9 +36,6 @@
 
 #include "Statement.h"
 
-struct sqlite3_context;
-struct Mem;  // This corresponds to the opaque type "sqlite3_value"
- 
 namespace Orthanc
 {
   namespace SQLite
@@ -50,14 +47,14 @@
     private:
       struct sqlite3_context* context_;
       unsigned int argc_;
-      struct ::Mem** argv_;
+      Internals::SQLiteValue** argv_;
 
       void CheckIndex(unsigned int index) const;
 
     public:
       FunctionContext(struct sqlite3_context* context,
                       int argc,
-                      struct ::Mem** argv);
+                      Internals::SQLiteValue** argv);
 
       ColumnType GetColumnType(unsigned int index) const;
  
--- a/Core/SQLite/IScalarFunction.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/IScalarFunction.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Redistribution and use in source and binary forms, with or without
--- a/Core/SQLite/ITransaction.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/ITransaction.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
--- a/Core/SQLite/NonCopyable.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/NonCopyable.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Redistribution and use in source and binary forms, with or without
--- a/Core/SQLite/OrthancSQLiteException.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/OrthancSQLiteException.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
@@ -38,6 +38,11 @@
 #pragma once
 
 
+#if ORTHANC_ENABLE_SQLITE != 1
+#  error Macro ORTHANC_ENABLE_SQLITE must be set to 1 to use SQLite
+#endif
+
+
 #if ORTHANC_SQLITE_STANDALONE == 1
 #include <stdexcept>
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/SQLite/SQLiteTypes.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,73 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *    * Neither the name of the CHU of Liege, nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ **/
+
+
+#pragma once
+
+struct sqlite3;
+struct sqlite3_context;
+struct sqlite3_stmt;
+
+#if !defined(ORTHANC_SQLITE_VERSION)
+#error  Please define macro ORTHANC_SQLITE_VERSION
+#endif
+
+
+/**
+ * "sqlite3_value" is defined as:
+ * - "typedef struct Mem sqlite3_value;" up to SQLite <= 3.18.2
+ * - "typedef struct sqlite3_value sqlite3_value;" since SQLite >= 3.19.0.
+ * We create our own copy of this typedef to get around this API incompatibility.
+ * https://github.com/mackyle/sqlite/commit/db1d90df06a78264775a14d22c3361eb5b42be17
+ **/
+      
+#if ORTHANC_SQLITE_VERSION < 3019000
+struct Mem;
+#else
+struct sqlite3_value;
+#endif
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    namespace Internals
+    {
+#if ORTHANC_SQLITE_VERSION < 3019000
+      typedef struct ::Mem  SQLiteValue;
+#else
+      typedef struct ::sqlite3_value  SQLiteValue;
+#endif
+    }
+  }
+}
--- a/Core/SQLite/Statement.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/Statement.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
@@ -46,8 +46,21 @@
 #include <stdio.h>
 #include <algorithm>
 
-#if ORTHANC_SQLITE_STANDALONE != 1
-#include "../Logging.h"
+#if (ORTHANC_SQLITE_STANDALONE == 1)
+// Trace logging is disabled if this SQLite wrapper is used
+// independently of Orthanc
+#  define LOG_CREATE(message);
+#  define LOG_APPLY(message);
+#elif defined(NDEBUG)
+// Trace logging is disabled in release builds
+#  include "../Logging.h"
+#  define LOG_CREATE(message);
+#  define LOG_APPLY(message);
+#else
+// Trace logging is enabled in debug builds
+#  include "../Logging.h"
+#  define LOG_CREATE(message)  VLOG(1) << "SQLite::Statement create: " << message;
+#  define LOG_APPLY(message);  // VLOG(1) << "SQLite::Statement apply: " << message;
 #endif
 
 #include "sqlite3.h"
@@ -56,6 +69,7 @@
 #define snprintf _snprintf
 #endif
 
+
 namespace Orthanc
 {
   namespace SQLite
@@ -103,6 +117,7 @@
       reference_(database.GetCachedStatement(id, sql.c_str()))
     {
       Reset(true);
+      LOG_CREATE(sql);
     }
 
 
@@ -112,6 +127,7 @@
       reference_(database.GetCachedStatement(id, sql))
     {
       Reset(true);
+      LOG_CREATE(sql);
     }
 
 
@@ -119,6 +135,7 @@
                          const std::string& sql) :
       reference_(database.GetWrappedObject(), sql.c_str())
     {
+      LOG_CREATE(sql);
     }
 
 
@@ -126,23 +143,20 @@
                          const char* sql) :
       reference_(database.GetWrappedObject(), sql)
     {
+      LOG_CREATE(sql);
     }
 
 
     bool Statement::Run()
     {
-#if ORTHANC_SQLITE_STANDALONE != 1
-      VLOG(1) << "SQLite::Statement::Run " << sqlite3_sql(GetStatement());
-#endif
+      LOG_APPLY(sqlite3_sql(GetStatement()));
 
       return CheckError(sqlite3_step(GetStatement()), ErrorCode_SQLiteCannotRun) == SQLITE_DONE;
     }
 
     bool Statement::Step()
     {
-#if ORTHANC_SQLITE_STANDALONE != 1
-      VLOG(1) << "SQLite::Statement::Step " << sqlite3_sql(GetStatement());
-#endif
+      LOG_APPLY(sqlite3_sql(GetStatement()));
 
       return CheckError(sqlite3_step(GetStatement()), ErrorCode_SQLiteCannotStep) == SQLITE_ROW;
     }
--- a/Core/SQLite/Statement.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/Statement.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
@@ -46,11 +46,9 @@
 #include <stdint.h>
 
 #if ORTHANC_BUILD_UNIT_TESTS == 1
-#include <gtest/gtest_prod.h>
+#  include <gtest/gtest_prod.h>
 #endif
 
-struct sqlite3_stmt;
-
 
 namespace Orthanc
 {
--- a/Core/SQLite/StatementId.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/StatementId.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
--- a/Core/SQLite/StatementId.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/StatementId.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
--- a/Core/SQLite/StatementReference.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/StatementReference.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
@@ -82,7 +82,8 @@
       if (error != SQLITE_OK)
       {
 #if ORTHANC_SQLITE_STANDALONE != 1
-        LOG(ERROR) << "SQLite: " << sqlite3_errmsg(database);
+        LOG(ERROR) << "SQLite: " << sqlite3_errmsg(database)
+                   << " (" << sqlite3_extended_errcode(database) << ")";
 #endif
 
         throw OrthancSQLiteException(ErrorCode_SQLitePrepareStatement);
--- a/Core/SQLite/StatementReference.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/StatementReference.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
@@ -38,13 +38,12 @@
 #pragma once
 
 #include "NonCopyable.h"
+#include "SQLiteTypes.h"
 
 #include <stdint.h>
 #include <cassert>
 #include <stdlib.h>
 
-struct sqlite3;
-struct sqlite3_stmt;
 
 namespace Orthanc
 {
--- a/Core/SQLite/Transaction.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/Transaction.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
--- a/Core/SQLite/Transaction.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/SQLite/Transaction.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
- * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
  * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/SerializationToolbox.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,375 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeaders.h"
+#include "SerializationToolbox.h"
+
+#include "OrthancException.h"
+
+namespace Orthanc
+{
+  namespace SerializationToolbox
+  {
+    std::string ReadString(const Json::Value& value,
+                           const std::string& field)
+    {
+      if (value.type() != Json::objectValue ||
+          !value.isMember(field.c_str()) ||
+          value[field.c_str()].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        return value[field.c_str()].asString();
+      }
+    }
+
+
+    int ReadInteger(const Json::Value& value,
+                    const std::string& field)
+    {
+      if (value.type() != Json::objectValue ||
+          !value.isMember(field.c_str()) ||
+          (value[field.c_str()].type() != Json::intValue &&
+           value[field.c_str()].type() != Json::uintValue))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        return value[field.c_str()].asInt();
+      }    
+    }
+
+
+    unsigned int ReadUnsignedInteger(const Json::Value& value,
+                                     const std::string& field)
+    {
+      int tmp = ReadInteger(value, field);
+
+      if (tmp < 0)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        return static_cast<unsigned int>(tmp);
+      }
+    }
+
+
+    bool ReadBoolean(const Json::Value& value,
+                     const std::string& field)
+    {
+      if (value.type() != Json::objectValue ||
+          !value.isMember(field.c_str()) ||
+          value[field.c_str()].type() != Json::booleanValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        return value[field.c_str()].asBool();
+      }   
+    }
+
+  
+    void ReadArrayOfStrings(std::vector<std::string>& target,
+                            const Json::Value& value,
+                            const std::string& field)
+    {
+      if (value.type() != Json::objectValue ||
+          !value.isMember(field.c_str()) ||
+          value[field.c_str()].type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      const Json::Value& arr = value[field.c_str()];
+
+      target.resize(arr.size());
+
+      for (Json::Value::ArrayIndex i = 0; i < arr.size(); i++)
+      {
+        if (arr[i].type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);        
+        }
+        else
+        {
+          target[i] = arr[i].asString();
+        }
+      }
+    }
+
+
+    void ReadListOfStrings(std::list<std::string>& target,
+                           const Json::Value& value,
+                           const std::string& field)
+    {
+      std::vector<std::string> tmp;
+      ReadArrayOfStrings(tmp, value, field);
+
+      target.clear();
+      for (size_t i = 0; i < tmp.size(); i++)
+      {
+        target.push_back(tmp[i]);
+      }
+    }
+  
+
+    void ReadSetOfStrings(std::set<std::string>& target,
+                          const Json::Value& value,
+                          const std::string& field)
+    {
+      std::vector<std::string> tmp;
+      ReadArrayOfStrings(tmp, value, field);
+
+      target.clear();
+      for (size_t i = 0; i < tmp.size(); i++)
+      {
+        target.insert(tmp[i]);
+      }
+    }
+
+
+    void ReadSetOfTags(std::set<DicomTag>& target,
+                       const Json::Value& value,
+                       const std::string& field)
+    {
+      if (value.type() != Json::objectValue ||
+          !value.isMember(field.c_str()) ||
+          value[field.c_str()].type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      const Json::Value& arr = value[field.c_str()];
+
+      target.clear();
+
+      for (Json::Value::ArrayIndex i = 0; i < arr.size(); i++)
+      {
+        DicomTag tag(0, 0);
+
+        if (arr[i].type() != Json::stringValue ||
+            !DicomTag::ParseHexadecimal(tag, arr[i].asCString()))
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);        
+        }
+        else
+        {
+          target.insert(tag);
+        }
+      }
+    }
+
+
+    void ReadMapOfStrings(std::map<std::string, std::string>& target,
+                          const Json::Value& value,
+                          const std::string& field)
+    {
+      if (value.type() != Json::objectValue ||
+          !value.isMember(field.c_str()) ||
+          value[field.c_str()].type() != Json::objectValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      const Json::Value& source = value[field.c_str()];
+
+      target.clear();
+
+      Json::Value::Members members = source.getMemberNames();
+
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        const Json::Value& tmp = source[members[i]];
+
+        if (tmp.type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);        
+        }
+        else
+        {
+          target[members[i]] = tmp.asString();
+        }
+      }
+    }
+
+
+    void ReadMapOfTags(std::map<DicomTag, std::string>& target,
+                       const Json::Value& value,
+                       const std::string& field)
+    {
+      if (value.type() != Json::objectValue ||
+          !value.isMember(field.c_str()) ||
+          value[field.c_str()].type() != Json::objectValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      const Json::Value& source = value[field.c_str()];
+
+      target.clear();
+
+      Json::Value::Members members = source.getMemberNames();
+
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        const Json::Value& tmp = source[members[i]];
+
+        DicomTag tag(0, 0);
+
+        if (!DicomTag::ParseHexadecimal(tag, members[i].c_str()) ||
+            tmp.type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);        
+        }
+        else
+        {
+          target[tag] = tmp.asString();
+        }
+      }
+    }
+
+
+    void WriteArrayOfStrings(Json::Value& target,
+                             const std::vector<std::string>& values,
+                             const std::string& field)
+    {
+      if (target.type() != Json::objectValue ||
+          target.isMember(field.c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      Json::Value& value = target[field];
+
+      value = Json::arrayValue;
+      for (size_t i = 0; i < values.size(); i++)
+      {
+        value.append(values[i]);
+      }
+    }
+
+
+    void WriteSetOfStrings(Json::Value& target,
+                           const std::set<std::string>& values,
+                           const std::string& field)
+    {
+      if (target.type() != Json::objectValue ||
+          target.isMember(field.c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      Json::Value& value = target[field];
+
+      value = Json::arrayValue;
+
+      for (std::set<std::string>::const_iterator it = values.begin();
+           it != values.end(); ++it)
+      {
+        value.append(*it);
+      }
+    }
+
+
+    void WriteSetOfTags(Json::Value& target,
+                        const std::set<DicomTag>& tags,
+                        const std::string& field)
+    {
+      if (target.type() != Json::objectValue ||
+          target.isMember(field.c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      Json::Value& value = target[field];
+
+      value = Json::arrayValue;
+
+      for (std::set<DicomTag>::const_iterator it = tags.begin();
+           it != tags.end(); ++it)
+      {
+        value.append(it->Format());
+      }
+    }
+
+
+    void WriteMapOfStrings(Json::Value& target,
+                           const std::map<std::string, std::string>& values,
+                           const std::string& field)
+    {
+      if (target.type() != Json::objectValue ||
+          target.isMember(field.c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      Json::Value& value = target[field];
+
+      value = Json::objectValue;
+
+      for (std::map<std::string, std::string>::const_iterator
+             it = values.begin(); it != values.end(); ++it)
+      {
+        value[it->first] = it->second;
+      }
+    }
+
+
+    void WriteMapOfTags(Json::Value& target,
+                        const std::map<DicomTag, std::string>& values,
+                        const std::string& field)
+    {
+      if (target.type() != Json::objectValue ||
+          target.isMember(field.c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      Json::Value& value = target[field];
+
+      value = Json::objectValue;
+
+      for (std::map<DicomTag, std::string>::const_iterator
+             it = values.begin(); it != values.end(); ++it)
+      {
+        value[it->first.Format()] = it->second;
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/SerializationToolbox.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,102 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomFormat/DicomTag.h"
+
+#include <json/value.h>
+#include <list>
+#include <map>
+
+namespace Orthanc
+{
+  namespace SerializationToolbox
+  {
+    std::string ReadString(const Json::Value& value,
+                           const std::string& field);
+
+    int ReadInteger(const Json::Value& value,
+                    const std::string& field);
+
+    unsigned int ReadUnsignedInteger(const Json::Value& value,
+                                     const std::string& field);
+
+    bool ReadBoolean(const Json::Value& value,
+                     const std::string& field);
+
+    void ReadArrayOfStrings(std::vector<std::string>& target,
+                            const Json::Value& value,
+                            const std::string& field);
+
+    void ReadListOfStrings(std::list<std::string>& target,
+                           const Json::Value& value,
+                           const std::string& field);
+
+    void ReadSetOfStrings(std::set<std::string>& target,
+                          const Json::Value& value,
+                          const std::string& field);
+
+    void ReadSetOfTags(std::set<DicomTag>& target,
+                       const Json::Value& value,
+                       const std::string& field);
+
+    void ReadMapOfStrings(std::map<std::string, std::string>& values,
+                          const Json::Value& target,
+                          const std::string& field);
+
+    void ReadMapOfTags(std::map<DicomTag, std::string>& values,
+                       const Json::Value& target,
+                       const std::string& field);
+
+    void WriteArrayOfStrings(Json::Value& target,
+                             const std::vector<std::string>& values,
+                             const std::string& field);
+
+    void WriteSetOfStrings(Json::Value& target,
+                           const std::set<std::string>& values,
+                           const std::string& field);
+
+    void WriteSetOfTags(Json::Value& target,
+                        const std::set<DicomTag>& tags,
+                        const std::string& field);
+
+    void WriteMapOfStrings(Json::Value& target,
+                           const std::map<std::string, std::string>& values,
+                           const std::string& field);
+
+    void WriteMapOfTags(Json::Value& target,
+                        const std::map<DicomTag, std::string>& values,
+                        const std::string& field);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/SharedLibrary.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,136 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeaders.h"
+#include "SharedLibrary.h"
+
+#include "Logging.h"
+#include "OrthancException.h"
+
+#include <boost/filesystem.hpp>
+
+#if defined(_WIN32)
+#include <windows.h>
+#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__)
+#include <dlfcn.h>
+#else
+#error Support your platform here
+#endif
+
+namespace Orthanc
+{
+  SharedLibrary::SharedLibrary(const std::string& path) : 
+    path_(path), 
+    handle_(NULL)
+  {
+#if defined(_WIN32)
+    handle_ = ::LoadLibraryA(path_.c_str());
+    if (handle_ == NULL)
+    {
+      LOG(ERROR) << "LoadLibrary(" << path_ << ") failed: Error " << ::GetLastError();
+      throw OrthancException(ErrorCode_SharedLibrary);
+    }
+
+#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__)
+    handle_ = ::dlopen(path_.c_str(), RTLD_NOW);
+    if (handle_ == NULL) 
+    {
+      std::string explanation;
+      const char *tmp = ::dlerror();
+      if (tmp)
+      {
+        explanation = ": Error " + std::string(tmp);
+      }
+
+      LOG(ERROR) << "dlopen(" << path_ << ") failed" << explanation;
+      throw OrthancException(ErrorCode_SharedLibrary);
+    }
+
+#else
+#error Support your platform here
+#endif   
+  }
+
+  SharedLibrary::~SharedLibrary()
+  {
+    if (handle_)
+    {
+#if defined(_WIN32)
+      ::FreeLibrary((HMODULE)handle_);
+#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__)
+      ::dlclose(handle_);
+#else
+#error Support your platform here
+#endif
+    }
+  }
+
+
+  SharedLibrary::FunctionPointer SharedLibrary::GetFunctionInternal(const std::string& name)
+  {
+    if (!handle_)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+#if defined(_WIN32)
+    return ::GetProcAddress((HMODULE)handle_, name.c_str());
+#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__)
+    return ::dlsym(handle_, name.c_str());
+#else
+#error Support your platform here
+#endif
+  }
+
+
+  SharedLibrary::FunctionPointer SharedLibrary::GetFunction(const std::string& name)
+  {
+    SharedLibrary::FunctionPointer result = GetFunctionInternal(name);
+  
+    if (result == NULL)
+    {
+      LOG(ERROR) << "Shared library does not expose function \"" << name << "\"";
+      throw OrthancException(ErrorCode_SharedLibrary);
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  bool SharedLibrary::HasFunction(const std::string& name)
+  {
+    return GetFunctionInternal(name) != NULL;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/SharedLibrary.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,82 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The namespace SystemToolbox cannot be used in sandboxed environments
+#endif
+
+#if defined(_WIN32)
+#include <windows.h>
+#endif
+
+#include <string>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class SharedLibrary : public boost::noncopyable
+  {
+  public:
+#if defined(_WIN32)
+    typedef FARPROC FunctionPointer;
+#else
+    typedef void* FunctionPointer;
+#endif
+
+  private:
+    std::string path_;
+    void *handle_;
+
+    FunctionPointer GetFunctionInternal(const std::string& name);
+
+  public:
+    explicit SharedLibrary(const std::string& path);
+
+    ~SharedLibrary();
+
+    const std::string& GetPath() const
+    {
+      return path_;
+    }
+
+    bool HasFunction(const std::string& name);
+
+    FunctionPointer GetFunction(const std::string& name);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/SystemToolbox.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,581 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeaders.h"
+#include "SystemToolbox.h"
+
+
+#if defined(_WIN32)
+#  include <windows.h>
+#  include <process.h>   // For "_spawnvp()" and "_getpid()"
+#else
+#  include <unistd.h>    // For "execvp()"
+#  include <sys/wait.h>  // For "waitpid()"
+#endif
+
+
+#if defined(__APPLE__) && defined(__MACH__)
+#  include <mach-o/dyld.h> /* _NSGetExecutablePath */
+#  include <limits.h>      /* PATH_MAX */
+#endif
+
+
+#if defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
+#  include <limits.h>      /* PATH_MAX */
+#  include <signal.h>
+#  include <unistd.h>
+#endif
+
+
+#if defined(__OpenBSD__)
+#  include <sys/sysctl.h>  // For "sysctl", "CTL_KERN" and "KERN_PROC_ARGS"
+#endif
+
+
+#include "Logging.h"
+#include "OrthancException.h"
+#include "Toolbox.h"
+
+#include <boost/filesystem.hpp>
+#include <boost/filesystem/fstream.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/thread.hpp>
+
+
+namespace Orthanc
+{
+  static bool finish_;
+  static ServerBarrierEvent barrierEvent_;
+
+#if defined(_WIN32)
+  static BOOL WINAPI ConsoleControlHandler(DWORD dwCtrlType)
+  {
+    // http://msdn.microsoft.com/en-us/library/ms683242(v=vs.85).aspx
+    finish_ = true;
+    return true;
+  }
+#else
+  static void SignalHandler(int signal)
+  {
+    if (signal == SIGHUP)
+    {
+      barrierEvent_ = ServerBarrierEvent_Reload;
+    }
+
+    finish_ = true;
+  }
+#endif
+
+
+  static ServerBarrierEvent ServerBarrierInternal(const bool* stopFlag)
+  {
+#if defined(_WIN32)
+    SetConsoleCtrlHandler(ConsoleControlHandler, true);
+#else
+    signal(SIGINT, SignalHandler);
+    signal(SIGQUIT, SignalHandler);
+    signal(SIGTERM, SignalHandler);
+    signal(SIGHUP, SignalHandler);
+#endif
+  
+    // Active loop that awakens every 100ms
+    finish_ = false;
+    barrierEvent_ = ServerBarrierEvent_Stop;
+    while (!(*stopFlag || finish_))
+    {
+      SystemToolbox::USleep(100 * 1000);
+    }
+
+#if defined(_WIN32)
+    SetConsoleCtrlHandler(ConsoleControlHandler, false);
+#else
+    signal(SIGINT, NULL);
+    signal(SIGQUIT, NULL);
+    signal(SIGTERM, NULL);
+    signal(SIGHUP, NULL);
+#endif
+
+    return barrierEvent_;
+  }
+
+
+  ServerBarrierEvent SystemToolbox::ServerBarrier(const bool& stopFlag)
+  {
+    return ServerBarrierInternal(&stopFlag);
+  }
+
+
+  ServerBarrierEvent SystemToolbox::ServerBarrier()
+  {
+    const bool stopFlag = false;
+    return ServerBarrierInternal(&stopFlag);
+  }
+
+
+  void SystemToolbox::USleep(uint64_t microSeconds)
+  {
+#if defined(_WIN32)
+    ::Sleep(static_cast<DWORD>(microSeconds / static_cast<uint64_t>(1000)));
+#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__native_client__)
+    usleep(microSeconds);
+#else
+#error Support your platform here
+#endif
+  }
+
+
+  static std::streamsize GetStreamSize(std::istream& f)
+  {
+    // http://www.cplusplus.com/reference/iostream/istream/tellg/
+    f.seekg(0, std::ios::end);
+    std::streamsize size = f.tellg();
+    f.seekg(0, std::ios::beg);
+
+    return size;
+  }
+
+
+  void SystemToolbox::ReadFile(std::string& content,
+                               const std::string& path) 
+  {
+    if (!IsRegularFile(path))
+    {
+      LOG(ERROR) << "The path does not point to a regular file: " << path;
+      throw OrthancException(ErrorCode_RegularFileExpected);
+    }
+
+    boost::filesystem::ifstream f;
+    f.open(path, std::ifstream::in | std::ifstream::binary);
+    if (!f.good())
+    {
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+
+    std::streamsize size = GetStreamSize(f);
+    content.resize(static_cast<size_t>(size));
+    if (size != 0)
+    {
+      f.read(reinterpret_cast<char*>(&content[0]), size);
+    }
+
+    f.close();
+  }
+
+
+  bool SystemToolbox::ReadHeader(std::string& header,
+                                 const std::string& path,
+                                 size_t headerSize)
+  {
+    if (!IsRegularFile(path))
+    {
+      LOG(ERROR) << "The path does not point to a regular file: " << path;
+      throw OrthancException(ErrorCode_RegularFileExpected);
+    }
+
+    boost::filesystem::ifstream f;
+    f.open(path, std::ifstream::in | std::ifstream::binary);
+    if (!f.good())
+    {
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+
+    bool full = true;
+
+    {
+      std::streamsize size = GetStreamSize(f);
+      if (size <= 0)
+      {
+        headerSize = 0;
+        full = false;
+      }
+      else if (static_cast<size_t>(size) < headerSize)
+      {
+        headerSize = static_cast<size_t>(size);  // Truncate to the size of the file
+        full = false;
+      }
+    }
+
+    header.resize(headerSize);
+    if (headerSize != 0)
+    {
+      f.read(reinterpret_cast<char*>(&header[0]), headerSize);
+    }
+
+    f.close();
+
+    return full;
+  }
+
+
+  void SystemToolbox::WriteFile(const void* content,
+                                size_t size,
+                                const std::string& path)
+  {
+    boost::filesystem::ofstream f;
+    f.open(path, std::ofstream::out | std::ofstream::binary);
+    if (!f.good())
+    {
+      throw OrthancException(ErrorCode_CannotWriteFile);
+    }
+
+    if (size != 0)
+    {
+      f.write(reinterpret_cast<const char*>(content), size);
+
+      if (!f.good())
+      {
+        f.close();
+        throw OrthancException(ErrorCode_FileStorageCannotWrite);
+      }
+    }
+
+    f.close();
+  }
+
+
+  void SystemToolbox::WriteFile(const std::string& content,
+                                const std::string& path)
+  {
+    WriteFile(content.size() > 0 ? content.c_str() : NULL,
+              content.size(), path);
+  }
+
+
+  void SystemToolbox::RemoveFile(const std::string& path)
+  {
+    if (boost::filesystem::exists(path))
+    {
+      if (IsRegularFile(path))
+      {
+        boost::filesystem::remove(path);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_RegularFileExpected);
+      }
+    }
+  }
+
+
+  uint64_t SystemToolbox::GetFileSize(const std::string& path)
+  {
+    try
+    {
+      return static_cast<uint64_t>(boost::filesystem::file_size(path));
+    }
+    catch (boost::filesystem::filesystem_error&)
+    {
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+  }
+
+
+  void SystemToolbox::MakeDirectory(const std::string& path)
+  {
+    if (boost::filesystem::exists(path))
+    {
+      if (!boost::filesystem::is_directory(path))
+      {
+        throw OrthancException(ErrorCode_DirectoryOverFile);
+      }
+    }
+    else
+    {
+      if (!boost::filesystem::create_directories(path))
+      {
+        throw OrthancException(ErrorCode_MakeDirectory);
+      }
+    }
+  }
+
+
+  bool SystemToolbox::IsExistingFile(const std::string& path)
+  {
+    return boost::filesystem::exists(path);
+  }
+
+
+#if defined(_WIN32)
+  static std::string GetPathToExecutableInternal()
+  {
+    // Yes, this is ugly, but there is no simple way to get the 
+    // required buffer size, so we use a big constant
+    std::vector<char> buffer(32768);
+    /*int bytes =*/ GetModuleFileNameA(NULL, &buffer[0], static_cast<DWORD>(buffer.size() - 1));
+    return std::string(&buffer[0]);
+  }
+
+#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
+  static std::string GetPathToExecutableInternal()
+  {
+    // NOTE: For FreeBSD, using KERN_PROC_PATHNAME might be a better alternative
+
+    std::vector<char> buffer(PATH_MAX + 1);
+    ssize_t bytes = readlink("/proc/self/exe", &buffer[0], buffer.size() - 1);
+    if (bytes == 0)
+    {
+      throw OrthancException(ErrorCode_PathToExecutable);
+    }
+
+    return std::string(&buffer[0]);
+  }
+
+#elif defined(__APPLE__) && defined(__MACH__)
+  static std::string GetPathToExecutableInternal()
+  {
+    char pathbuf[PATH_MAX + 1];
+    unsigned int  bufsize = static_cast<int>(sizeof(pathbuf));
+
+    _NSGetExecutablePath( pathbuf, &bufsize);
+
+    return std::string(pathbuf);
+  }
+
+#elif defined(__OpenBSD__)
+  static std::string GetPathToExecutableInternal()
+  {
+    // This is an adapted version of the patch proposed in issue #64
+    // without an explicit call to "malloc()" to prevent memory leak
+    // https://bitbucket.org/sjodogne/orthanc/issues/64/add-openbsd-support
+    // https://stackoverflow.com/q/31494901/881731
+
+    const int mib[4] = { CTL_KERN, KERN_PROC_ARGS, getpid(), KERN_PROC_ARGV };
+
+    size_t len;
+    if (sysctl(mib, 4, NULL, &len, NULL, 0) == -1) 
+    {
+      throw OrthancException(ErrorCode_PathToExecutable);
+    }
+
+    std::string tmp;
+    tmp.resize(len);
+
+    char** buffer = reinterpret_cast<char**>(&tmp[0]);
+
+    if (sysctl(mib, 4, buffer, &len, NULL, 0) == -1) 
+    {
+      throw OrthancException(ErrorCode_PathToExecutable);
+    }
+    else
+    {
+      return std::string(buffer[0]);
+    }
+  }
+
+#else
+#error Support your platform here
+#endif
+
+
+  std::string SystemToolbox::GetPathToExecutable()
+  {
+    boost::filesystem::path p(GetPathToExecutableInternal());
+    return boost::filesystem::absolute(p).string();
+  }
+
+
+  std::string SystemToolbox::GetDirectoryOfExecutable()
+  {
+    boost::filesystem::path p(GetPathToExecutableInternal());
+    return boost::filesystem::absolute(p.parent_path()).string();
+  }
+
+
+  void SystemToolbox::ExecuteSystemCommand(const std::string& command,
+                                           const std::vector<std::string>& arguments)
+  {
+    // Convert the arguments as a C array
+    std::vector<char*>  args(arguments.size() + 2);
+
+    args.front() = const_cast<char*>(command.c_str());
+
+    for (size_t i = 0; i < arguments.size(); i++)
+    {
+      args[i + 1] = const_cast<char*>(arguments[i].c_str());
+    }
+
+    args.back() = NULL;
+
+    int status;
+
+#if defined(_WIN32)
+    // http://msdn.microsoft.com/en-us/library/275khfab.aspx
+    status = static_cast<int>(_spawnvp(_P_OVERLAY, command.c_str(), &args[0]));
+
+#else
+    int pid = fork();
+
+    if (pid == -1)
+    {
+      // Error in fork()
+#if ORTHANC_ENABLE_LOGGING == 1
+      LOG(ERROR) << "Cannot fork a child process";
+#endif
+
+      throw OrthancException(ErrorCode_SystemCommand);
+    }
+    else if (pid == 0)
+    {
+      // Execute the system command in the child process
+      execvp(command.c_str(), &args[0]);
+
+      // We should never get here
+      _exit(1);
+    }
+    else
+    {
+      // Wait for the system command to exit
+      waitpid(pid, &status, 0);
+    }
+#endif
+
+    if (status != 0)
+    {
+#if ORTHANC_ENABLE_LOGGING == 1
+      LOG(ERROR) << "System command failed with status code " << status;
+#endif
+
+      throw OrthancException(ErrorCode_SystemCommand);
+    }
+  }
+
+
+  int SystemToolbox::GetProcessId()
+  {
+#if defined(_WIN32)
+    return static_cast<int>(_getpid());
+#else
+    return static_cast<int>(getpid());
+#endif
+  }
+
+
+  bool SystemToolbox::IsRegularFile(const std::string& path)
+  {
+    namespace fs = boost::filesystem;
+
+    try
+    {
+      if (fs::exists(path))
+      {
+        fs::file_status status = fs::status(path);
+        return (status.type() == boost::filesystem::regular_file ||
+                status.type() == boost::filesystem::reparse_file);   // Fix BitBucket issue #11
+      }
+    }
+    catch (fs::filesystem_error&)
+    {
+    }
+
+    return false;
+  }
+
+
+  FILE* SystemToolbox::OpenFile(const std::string& path,
+                                FileMode mode)
+  {
+#if defined(_WIN32)
+    // TODO Deal with special characters by converting to the current locale
+#endif
+
+    const char* m;
+    switch (mode)
+    {
+      case FileMode_ReadBinary:
+        m = "rb";
+        break;
+
+      case FileMode_WriteBinary:
+        m = "wb";
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    return fopen(path.c_str(), m);
+  }
+
+
+  static boost::posix_time::ptime GetNow(bool utc)
+  {
+    if (utc)
+    {
+      return boost::posix_time::second_clock::universal_time();
+    }
+    else
+    {
+      return boost::posix_time::second_clock::local_time();
+    }
+  }
+
+
+  std::string SystemToolbox::GetNowIsoString(bool utc)
+  {
+    return boost::posix_time::to_iso_string(GetNow(utc));
+  }
+
+  
+  void SystemToolbox::GetNowDicom(std::string& date,
+                                  std::string& time,
+                                  bool utc)
+  {
+    boost::posix_time::ptime now = GetNow(utc);
+    tm tm = boost::posix_time::to_tm(now);
+
+    char s[32];
+    sprintf(s, "%04d%02d%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
+    date.assign(s);
+
+    // TODO milliseconds
+    sprintf(s, "%02d%02d%02d.%06d", tm.tm_hour, tm.tm_min, tm.tm_sec, 0);
+    time.assign(s);
+  }
+
+  
+  unsigned int SystemToolbox::GetHardwareConcurrency()
+  {
+    // Get 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)
+    {
+      return 1;
+    }
+    else
+    {
+      return threads;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/SystemToolbox.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,104 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The namespace SystemToolbox cannot be used in sandboxed environments
+#endif
+
+#include "Enumerations.h"
+
+#include <vector>
+#include <string>
+#include <stdint.h>
+
+namespace Orthanc
+{
+  namespace SystemToolbox
+  {
+    void USleep(uint64_t microSeconds);
+
+    ServerBarrierEvent ServerBarrier(const bool& stopFlag);
+
+    ServerBarrierEvent ServerBarrier();
+
+    void ReadFile(std::string& content,
+                  const std::string& path);
+
+    bool ReadHeader(std::string& header,
+                    const std::string& path,
+                    size_t headerSize);
+
+    void WriteFile(const void* content,
+                   size_t size,
+                   const std::string& path);
+
+    void WriteFile(const std::string& content,
+                   const std::string& path);
+
+    void RemoveFile(const std::string& path);
+
+    uint64_t GetFileSize(const std::string& path);
+
+    void MakeDirectory(const std::string& path);
+
+    bool IsExistingFile(const std::string& path);
+
+    std::string GetPathToExecutable();
+
+    std::string GetDirectoryOfExecutable();
+
+    void ExecuteSystemCommand(const std::string& command,
+                              const std::vector<std::string>& arguments);
+
+    int GetProcessId();
+
+    bool IsRegularFile(const std::string& path);
+
+    FILE* OpenFile(const std::string& path,
+                   FileMode mode);
+
+    std::string GetNowIsoString(bool utc);
+
+    void GetNowDicom(std::string& date,
+                     std::string& time,
+                     bool utc);
+
+    unsigned int GetHardwareConcurrency();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/TemporaryFile.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,95 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeaders.h"
+#include "TemporaryFile.h"
+
+#include "SystemToolbox.h"
+#include "Toolbox.h"
+
+#include <boost/filesystem.hpp>
+
+namespace Orthanc
+{
+  static std::string CreateTemporaryPath(const char* extension)
+  {
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+    boost::filesystem::path tmpDir = boost::filesystem::temp_directory_path();
+#elif defined(__linux__)
+    boost::filesystem::path tmpDir("/tmp");
+#else
+#error Support your platform here
+#endif
+
+    // We use UUID to create unique path to temporary files
+    std::string filename = "Orthanc-" + Orthanc::Toolbox::GenerateUuid();
+
+    if (extension != NULL)
+    {
+      filename.append(extension);
+    }
+
+    tmpDir /= filename;
+    return tmpDir.string();
+  }
+
+
+  TemporaryFile::TemporaryFile() : 
+    path_(CreateTemporaryPath(NULL))
+  {
+  }
+
+
+  TemporaryFile::TemporaryFile(const char* extension) :
+    path_(CreateTemporaryPath(extension))
+  {
+  }
+
+
+  TemporaryFile::~TemporaryFile()
+  {
+    boost::filesystem::remove(path_);
+  }
+
+
+  void TemporaryFile::Write(const std::string& content)
+  {
+    SystemToolbox::WriteFile(content, path_);
+  }
+
+
+  void TemporaryFile::Read(std::string& content) const
+  {
+    SystemToolbox::ReadFile(content, path_);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/TemporaryFile.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,69 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The class TemporaryFile cannot be used in sandboxed environments
+#endif
+
+#include <string>
+
+namespace Orthanc
+{
+  class TemporaryFile
+  {
+  private:
+    std::string path_;
+
+  public:
+    TemporaryFile();
+
+    TemporaryFile(const char* extension);
+
+    ~TemporaryFile();
+
+    const std::string& GetPath() const
+    {
+      return path_;
+    }
+
+    void Write(const std::string& content);
+
+    void Read(std::string& content) const;
+  };
+}
--- a/Core/Toolbox.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Toolbox.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -36,57 +37,38 @@
 #include "OrthancException.h"
 #include "Logging.h"
 
+#include <boost/algorithm/string/case_conv.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/regex.hpp> 
+#include <boost/uuid/sha1.hpp>
+ 
 #include <string>
 #include <stdint.h>
 #include <string.h>
-#include <boost/filesystem.hpp>
-#include <boost/filesystem/fstream.hpp>
-#include <boost/uuid/sha1.hpp>
-#include <boost/lexical_cast.hpp>
 #include <algorithm>
 #include <ctype.h>
 
-#if BOOST_HAS_DATE_TIME == 1
-#include <boost/date_time/posix_time/posix_time.hpp>
-#endif
 
-#if BOOST_HAS_REGEX == 1
-#include <boost/regex.hpp> 
+#if ORTHANC_ENABLE_MD5 == 1
+#  include "../Resources/ThirdParty/md5/md5.h"
 #endif
 
-#if defined(_WIN32)
-#include <windows.h>
-#include <process.h>   // For "_spawnvp()" and "_getpid()"
-#else
-#include <unistd.h>    // For "execvp()"
-#include <sys/wait.h>  // For "waitpid()"
-#endif
-
-#if defined(__APPLE__) && defined(__MACH__)
-#include <mach-o/dyld.h> /* _NSGetExecutablePath */
-#include <limits.h>      /* PATH_MAX */
+#if ORTHANC_ENABLE_BASE64 == 1
+#  include "../Resources/ThirdParty/base64/base64.h"
 #endif
 
-#if defined(__linux) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
-#include <limits.h>      /* PATH_MAX */
-#include <signal.h>
-#include <unistd.h>
-#endif
-
-#if BOOST_HAS_LOCALE != 1
-#error Since version 0.7.6, Orthanc entirely relies on boost::locale
+#if ORTHANC_ENABLE_LOCALE == 1
+#  include <boost/locale.hpp>
 #endif
 
-#include <boost/locale.hpp>
-
-
-#if !defined(ORTHANC_ENABLE_MD5) || ORTHANC_ENABLE_MD5 == 1
-#include "../Resources/ThirdParty/md5/md5.h"
-#endif
-
-
-#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1
-#include "../Resources/ThirdParty/base64/base64.h"
+#if ORTHANC_ENABLE_SSL == 1
+// For OpenSSL initialization and finalization
+#  include <openssl/conf.h>
+#  include <openssl/engine.h>
+#  include <openssl/err.h>
+#  include <openssl/evp.h>
+#  include <openssl/ssl.h>
 #endif
 
 
@@ -103,81 +85,105 @@
 #endif
 
 
-#if ORTHANC_PUGIXML_ENABLED == 1
-#include "ChunkedBuffer.h"
-#include <pugixml.hpp>
+#if defined(_WIN32)
+#  include <windows.h>   // For ::Sleep
+#endif
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+#  include "ChunkedBuffer.h"
+#  include <pugixml.hpp>
 #endif
 
 
+// Inclusions for UUID
+// http://stackoverflow.com/a/1626302
+
+extern "C"
+{
+#if defined(_WIN32)
+#  include <rpc.h>
+#else
+#  include <uuid/uuid.h>
+#endif
+}
+
+
+
 namespace Orthanc
 {
-  static bool finish;
-
-#if defined(_WIN32)
-  static BOOL WINAPI ConsoleControlHandler(DWORD dwCtrlType)
-  {
-    // http://msdn.microsoft.com/en-us/library/ms683242(v=vs.85).aspx
-    finish = true;
-    return true;
-  }
-#else
-  static void SignalHandler(int)
+  void Toolbox::LinesIterator::FindEndOfLine()
   {
-    finish = true;
+    lineEnd_ = lineStart_;
+
+    while (lineEnd_ < content_.size() &&
+           content_[lineEnd_] != '\n' &&
+           content_[lineEnd_] != '\r')
+    {
+      lineEnd_ += 1;
+    }
   }
-#endif
+  
 
-  void Toolbox::USleep(uint64_t microSeconds)
+  Toolbox::LinesIterator::LinesIterator(const std::string& content) :
+    content_(content),
+    lineStart_(0)
   {
-#if defined(_WIN32)
-    ::Sleep(static_cast<DWORD>(microSeconds / static_cast<uint64_t>(1000)));
-#elif defined(__linux) || defined(__APPLE__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
-    usleep(microSeconds);
-#else
-#error Support your platform here
-#endif
+    FindEndOfLine();
   }
 
-
-  static void ServerBarrierInternal(const bool* stopFlag)
+    
+  bool Toolbox::LinesIterator::GetLine(std::string& target) const
   {
-#if defined(_WIN32)
-    SetConsoleCtrlHandler(ConsoleControlHandler, true);
-#else
-    signal(SIGINT, SignalHandler);
-    signal(SIGQUIT, SignalHandler);
-    signal(SIGTERM, SignalHandler);
-#endif
-  
-    // Active loop that awakens every 100ms
-    finish = false;
-    while (!(*stopFlag || finish))
+    assert(lineStart_ <= content_.size() &&
+           lineEnd_ <= content_.size() &&
+           lineStart_ <= lineEnd_);
+
+    if (lineStart_ == content_.size())
     {
-      Toolbox::USleep(100 * 1000);
+      return false;
     }
-
-#if defined(_WIN32)
-    SetConsoleCtrlHandler(ConsoleControlHandler, false);
-#else
-    signal(SIGINT, NULL);
-    signal(SIGQUIT, NULL);
-    signal(SIGTERM, NULL);
-#endif
+    else
+    {
+      target = content_.substr(lineStart_, lineEnd_ - lineStart_);
+      return true;
+    }
   }
 
+    
+  void Toolbox::LinesIterator::Next()
+  {
+    lineStart_ = lineEnd_;
 
-  void Toolbox::ServerBarrier(const bool& stopFlag)
-  {
-    ServerBarrierInternal(&stopFlag);
+    if (lineStart_ != content_.size())
+    {
+      assert(content_[lineStart_] == '\r' ||
+             content_[lineStart_] == '\n');
+
+      char second;
+      
+      if (content_[lineStart_] == '\r')
+      {
+        second = '\n';
+      }
+      else
+      {
+        second = '\r';
+      }
+        
+      lineStart_ += 1;
+
+      if (lineStart_ < content_.size() &&
+          content_[lineStart_] == second)
+      {
+        lineStart_ += 1;
+      }
+
+      FindEndOfLine();
+    }
   }
 
-  void Toolbox::ServerBarrier()
-  {
-    const bool stopFlag = false;
-    ServerBarrierInternal(&stopFlag);
-  }
-
-
+  
   void Toolbox::ToUpperCase(std::string& s)
   {
     std::transform(s.begin(), s.end(), s.begin(), toupper);
@@ -205,82 +211,6 @@
   }
 
 
-  void Toolbox::ReadFile(std::string& content,
-                         const std::string& path) 
-  {
-    if (!boost::filesystem::is_regular_file(path))
-    {
-      LOG(ERROR) << "The path does not point to a regular file: " << path;
-      throw OrthancException(ErrorCode_RegularFileExpected);
-    }
-
-    boost::filesystem::ifstream f;
-    f.open(path, std::ifstream::in | std::ifstream::binary);
-    if (!f.good())
-    {
-      throw OrthancException(ErrorCode_InexistentFile);
-    }
-
-    // http://www.cplusplus.com/reference/iostream/istream/tellg/
-    f.seekg(0, std::ios::end);
-    std::streamsize size = f.tellg();
-    f.seekg(0, std::ios::beg);
-
-    content.resize(size);
-    if (size != 0)
-    {
-      f.read(reinterpret_cast<char*>(&content[0]), size);
-    }
-
-    f.close();
-  }
-
-
-  void Toolbox::WriteFile(const void* content,
-                          size_t size,
-                          const std::string& path)
-  {
-    boost::filesystem::ofstream f;
-    f.open(path, std::ofstream::binary);
-    if (!f.good())
-    {
-      throw OrthancException(ErrorCode_CannotWriteFile);
-    }
-
-    if (size != 0)
-    {
-      f.write(reinterpret_cast<const char*>(content), size);
-    }
-
-    f.close();
-  }
-
-
-  void Toolbox::WriteFile(const std::string& content,
-                          const std::string& path)
-  {
-    WriteFile(content.size() > 0 ? content.c_str() : NULL,
-              content.size(), path);
-  }
-
-
-  void Toolbox::RemoveFile(const std::string& path)
-  {
-    if (boost::filesystem::exists(path))
-    {
-      if (boost::filesystem::is_regular_file(path))
-      {
-        boost::filesystem::remove(path);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_RegularFileExpected);
-      }
-    }
-  }
-
-
-
   void Toolbox::SplitUriComponents(UriComponents& components,
                                    const std::string& uri)
   {
@@ -413,6 +343,8 @@
         contentType = "application/json";
       else if (!strcmp(extension, "pdf"))
         contentType = "application/pdf";
+      else if (!strcmp(extension, "wasm"))
+        contentType = "application/wasm";
 
       // Images types
       else if (!strcmp(extension, "jpg") || !strcmp(extension, "jpeg"))
@@ -448,21 +380,7 @@
   }
 
 
-
-  uint64_t Toolbox::GetFileSize(const std::string& path)
-  {
-    try
-    {
-      return static_cast<uint64_t>(boost::filesystem::file_size(path));
-    }
-    catch (boost::filesystem::filesystem_error&)
-    {
-      throw OrthancException(ErrorCode_InexistentFile);
-    }
-  }
-
-
-#if !defined(ORTHANC_ENABLE_MD5) || ORTHANC_ENABLE_MD5 == 1
+#if ORTHANC_ENABLE_MD5 == 1
   static char GetHexadecimalCharacter(uint8_t value)
   {
     assert(value < 16);
@@ -494,16 +412,16 @@
 
   void Toolbox::ComputeMD5(std::string& result,
                            const void* data,
-                           size_t length)
+                           size_t size)
   {
     md5_state_s state;
     md5_init(&state);
 
-    if (length > 0)
+    if (size > 0)
     {
       md5_append(&state, 
                  reinterpret_cast<const md5_byte_t*>(data), 
-                 static_cast<int>(length));
+                 static_cast<int>(size));
     }
 
     md5_byte_t actualHash[16];
@@ -519,7 +437,7 @@
 #endif
 
 
-#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1
+#if ORTHANC_ENABLE_BASE64 == 1
   void Toolbox::EncodeBase64(std::string& result, 
                              const std::string& data)
   {
@@ -529,12 +447,23 @@
   void Toolbox::DecodeBase64(std::string& result, 
                              const std::string& data)
   {
+    for (size_t i = 0; i < data.length(); i++)
+    {
+      if (!isalnum(data[i]) &&
+          data[i] != '+' &&
+          data[i] != '/' &&
+          data[i] != '=')
+      {
+        // This is not a valid character for a Base64 string
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+
     result = base64_decode(data);
   }
 
 
-#  if BOOST_HAS_REGEX == 1
-  void Toolbox::DecodeDataUriScheme(std::string& mime,
+  bool Toolbox::DecodeDataUriScheme(std::string& mime,
                                     std::string& content,
                                     const std::string& source)
   {
@@ -546,71 +475,26 @@
     {
       mime = what[1];
       DecodeBase64(content, what[2]);
+      return true;
     }
     else
     {
-      throw OrthancException(ErrorCode_BadFileFormat);
+      return false;
     }
   }
-#  endif
+
+
+  void Toolbox::EncodeDataUriScheme(std::string& result,
+                                    const std::string& mime,
+                                    const std::string& content)
+  {
+    result = "data:" + mime + ";base64," + base64_encode(content);
+  }
 
 #endif
 
 
-
-#if defined(_WIN32)
-  static std::string GetPathToExecutableInternal()
-  {
-    // Yes, this is ugly, but there is no simple way to get the 
-    // required buffer size, so we use a big constant
-    std::vector<char> buffer(32768);
-    /*int bytes =*/ GetModuleFileNameA(NULL, &buffer[0], static_cast<DWORD>(buffer.size() - 1));
-    return std::string(&buffer[0]);
-  }
-
-#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
-  static std::string GetPathToExecutableInternal()
-  {
-    std::vector<char> buffer(PATH_MAX + 1);
-    ssize_t bytes = readlink("/proc/self/exe", &buffer[0], buffer.size() - 1);
-    if (bytes == 0)
-    {
-      throw OrthancException(ErrorCode_PathToExecutable);
-    }
-
-    return std::string(&buffer[0]);
-  }
-
-#elif defined(__APPLE__) && defined(__MACH__)
-  static std::string GetPathToExecutableInternal()
-  {
-    char pathbuf[PATH_MAX + 1];
-    unsigned int  bufsize = static_cast<int>(sizeof(pathbuf));
-
-    _NSGetExecutablePath( pathbuf, &bufsize);
-
-    return std::string(pathbuf);
-  }
-
-#else
-#error Support your platform here
-#endif
-
-
-  std::string Toolbox::GetPathToExecutable()
-  {
-    boost::filesystem::path p(GetPathToExecutableInternal());
-    return boost::filesystem::absolute(p).string();
-  }
-
-
-  std::string Toolbox::GetDirectoryOfExecutable()
-  {
-    boost::filesystem::path p(GetPathToExecutableInternal());
-    return boost::filesystem::absolute(p.parent_path()).string();
-  }
-
-
+#if ORTHANC_ENABLE_LOCALE == 1
   static const char* GetBoostLocaleEncoding(const Encoding sourceEncoding)
   {
     switch (sourceEncoding)
@@ -677,8 +561,10 @@
         throw OrthancException(ErrorCode_NotImplemented);
     }
   }
+#endif
 
 
+#if ORTHANC_ENABLE_LOCALE == 1
   std::string Toolbox::ConvertToUtf8(const std::string& source,
                                      Encoding sourceEncoding)
   {
@@ -705,8 +591,10 @@
       return ConvertToAscii(source);
     }
   }
+#endif
+  
 
-
+#if ORTHANC_ENABLE_LOCALE == 1
   std::string Toolbox::ConvertFromUtf8(const std::string& source,
                                        Encoding targetEncoding)
   {
@@ -733,8 +621,32 @@
       return ConvertToAscii(source);
     }
   }
+#endif
 
 
+  bool Toolbox::IsAsciiString(const void* data,
+                              size_t size)
+  {
+    const uint8_t* p = reinterpret_cast<const uint8_t*>(data);
+
+    for (size_t i = 0; i < size; i++, p++)
+    {
+      if (*p > 127 || *p == 0 || iscntrl(*p))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  bool Toolbox::IsAsciiString(const std::string& s)
+  {
+    return IsAsciiString(s.c_str(), s.size());
+  }
+  
+
   std::string Toolbox::ConvertToAscii(const std::string& source)
   {
     std::string result;
@@ -751,14 +663,16 @@
     return result;
   }
 
+
   void Toolbox::ComputeSHA1(std::string& result,
-                            const std::string& data)
+                            const void* data,
+                            size_t size)
   {
     boost::uuids::detail::sha1 sha1;
 
-    if (data.size() > 0)
+    if (size > 0)
     {
-      sha1.process_bytes(&data[0], data.size());
+      sha1.process_bytes(data, size);
     }
 
     unsigned int digest[5];
@@ -777,6 +691,20 @@
             digest[4]);
   }
 
+  void Toolbox::ComputeSHA1(std::string& result,
+                            const std::string& data)
+  {
+    if (data.size() > 0)
+    {
+      ComputeSHA1(result, data.c_str(), data.size());
+    }
+    else
+    {
+      ComputeSHA1(result, NULL, 0);
+    }
+  }
+
+
   bool Toolbox::IsSHA1(const char* str,
                        size_t size)
   {
@@ -855,30 +783,6 @@
   }
 
 
-#if BOOST_HAS_DATE_TIME == 1
-  std::string Toolbox::GetNowIsoString()
-  {
-    boost::posix_time::ptime now = boost::posix_time::second_clock::local_time();
-    return boost::posix_time::to_iso_string(now);
-  }
-
-  void Toolbox::GetNowDicom(std::string& date,
-                            std::string& time)
-  {
-    boost::posix_time::ptime now = boost::posix_time::second_clock::local_time();
-    tm tm = boost::posix_time::to_tm(now);
-
-    char s[32];
-    sprintf(s, "%04d%02d%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
-    date.assign(s);
-
-    // TODO milliseconds
-    sprintf(s, "%02d%02d%02d.%06d", tm.tm_hour, tm.tm_min, tm.tm_sec, 0);
-    time.assign(s);
-  }
-#endif
-
-
   std::string Toolbox::StripSpaces(const std::string& source)
   {
     size_t first = 0;
@@ -979,7 +883,6 @@
   }
 
 
-#if BOOST_HAS_REGEX == 1
   std::string Toolbox::WildcardToRegularExpression(const std::string& source)
   {
     // TODO - Speed up this with a regular expression
@@ -1007,8 +910,6 @@
 
     return result;
   }
-#endif
-
 
 
   void Toolbox::TokenizeString(std::vector<std::string>& result,
@@ -1036,32 +937,7 @@
   }
 
 
-  void Toolbox::MakeDirectory(const std::string& path)
-  {
-    if (boost::filesystem::exists(path))
-    {
-      if (!boost::filesystem::is_directory(path))
-      {
-        throw OrthancException(ErrorCode_DirectoryOverFile);
-      }
-    }
-    else
-    {
-      if (!boost::filesystem::create_directories(path))
-      {
-        throw OrthancException(ErrorCode_MakeDirectory);
-      }
-    }
-  }
-
-
-  bool Toolbox::IsExistingFile(const std::string& path)
-  {
-    return boost::filesystem::exists(path);
-  }
-
-
-#if ORTHANC_PUGIXML_ENABLED == 1
+#if ORTHANC_ENABLE_PUGIXML == 1
   class ChunkedBufferWriter : public pugi::xml_writer
   {
   private:
@@ -1183,64 +1059,6 @@
 #endif
 
 
-  void Toolbox::ExecuteSystemCommand(const std::string& command,
-                                     const std::vector<std::string>& arguments)
-  {
-    // Convert the arguments as a C array
-    std::vector<char*>  args(arguments.size() + 2);
-
-    args.front() = const_cast<char*>(command.c_str());
-
-    for (size_t i = 0; i < arguments.size(); i++)
-    {
-      args[i + 1] = const_cast<char*>(arguments[i].c_str());
-    }
-
-    args.back() = NULL;
-
-    int status;
-
-#if defined(_WIN32)
-    // http://msdn.microsoft.com/en-us/library/275khfab.aspx
-    status = static_cast<int>(_spawnvp(_P_OVERLAY, command.c_str(), &args[0]));
-
-#else
-    int pid = fork();
-
-    if (pid == -1)
-    {
-      // Error in fork()
-#if ORTHANC_ENABLE_LOGGING == 1
-      LOG(ERROR) << "Cannot fork a child process";
-#endif
-
-      throw OrthancException(ErrorCode_SystemCommand);
-    }
-    else if (pid == 0)
-    {
-      // Execute the system command in the child process
-      execvp(command.c_str(), &args[0]);
-
-      // We should never get here
-      _exit(1);
-    }
-    else
-    {
-      // Wait for the system command to exit
-      waitpid(pid, &status, 0);
-    }
-#endif
-
-    if (status != 0)
-    {
-#if ORTHANC_ENABLE_LOGGING == 1
-      LOG(ERROR) << "System command failed with status code " << status;
-#endif
-
-      throw OrthancException(ErrorCode_SystemCommand);
-    }
-  }
-
   
   bool Toolbox::IsInteger(const std::string& str)
   {
@@ -1348,15 +1166,402 @@
       return str.compare(0, prefix.size(), prefix) == 0;
     }
   }
+  
+
+  static bool IsUnreservedCharacter(char c)
+  {
+    // This function checks whether "c" is an unserved character
+    // wrt. an URI percent-encoding
+    // https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding%5Fin%5Fa%5FURI
+
+    return ((c >= 'A' && c <= 'Z') ||
+            (c >= 'a' && c <= 'z') ||
+            (c >= '0' && c <= '9') ||
+            c == '-' ||
+            c == '_' ||
+            c == '.' ||
+            c == '~');
+  }
+
+  void Toolbox::UriEncode(std::string& target,
+                          const std::string& source)
+  {
+    // Estimate the length of the percent-encoded URI
+    size_t length = 0;
+
+    for (size_t i = 0; i < source.size(); i++)
+    {
+      if (IsUnreservedCharacter(source[i]))
+      {
+        length += 1;
+      }
+      else
+      {
+        // This character must be percent-encoded
+        length += 3;
+      }
+    }
+
+    target.clear();
+    target.reserve(length);
+
+    for (size_t i = 0; i < source.size(); i++)
+    {
+      if (IsUnreservedCharacter(source[i]))
+      {
+        target.push_back(source[i]);
+      }
+      else
+      {
+        // This character must be percent-encoded
+        uint8_t byte = static_cast<uint8_t>(source[i]);
+        uint8_t a = byte >> 4;
+        uint8_t b = byte & 0x0f;
+
+        target.push_back('%');
+        target.push_back(a < 10 ? a + '0' : a - 10 + 'A');
+        target.push_back(b < 10 ? b + '0' : b - 10 + 'A');
+      }
+    }
+  }
+
+
+  static bool HasField(const Json::Value& json,
+                       const std::string& key,
+                       Json::ValueType expectedType)
+  {
+    if (json.type() != Json::objectValue ||
+        !json.isMember(key))
+    {
+      return false;
+    }
+    else if (json[key].type() == expectedType)
+    {
+      return true;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+  }
+
+
+  std::string Toolbox::GetJsonStringField(const Json::Value& json,
+                                          const std::string& key,
+                                          const std::string& defaultValue)
+  {
+    if (HasField(json, key, Json::stringValue))
+    {
+      return json[key].asString();
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  bool Toolbox::GetJsonBooleanField(const ::Json::Value& json,
+                                    const std::string& key,
+                                    bool defaultValue)
+  {
+    if (HasField(json, key, Json::booleanValue))
+    {
+      return json[key].asBool();
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  int Toolbox::GetJsonIntegerField(const ::Json::Value& json,
+                                   const std::string& key,
+                                   int defaultValue)
+  {
+    if (HasField(json, key, Json::intValue))
+    {
+      return json[key].asInt();
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  unsigned int Toolbox::GetJsonUnsignedIntegerField(const ::Json::Value& json,
+                                                    const std::string& key,
+                                                    unsigned int defaultValue)
+  {
+    int v = GetJsonIntegerField(json, key, defaultValue);
+
+    if (v < 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return static_cast<unsigned int>(v);
+    }
+  }
+
+
+  bool Toolbox::IsUuid(const std::string& str)
+  {
+    if (str.size() != 36)
+    {
+      return false;
+    }
+
+    for (size_t i = 0; i < str.length(); i++)
+    {
+      if (i == 8 || i == 13 || i == 18 || i == 23)
+      {
+        if (str[i] != '-')
+          return false;
+      }
+      else
+      {
+        if (!isalnum(str[i]))
+          return false;
+      }
+    }
+
+    return true;
+  }
 
 
-  int Toolbox::GetProcessId()
+  bool Toolbox::StartsWithUuid(const std::string& str)
+  {
+    if (str.size() < 36)
+    {
+      return false;
+    }
+
+    if (str.size() == 36)
+    {
+      return IsUuid(str);
+    }
+
+    assert(str.size() > 36);
+    if (!isspace(str[36]))
+    {
+      return false;
+    }
+
+    return IsUuid(str.substr(0, 36));
+  }
+
+
+#if ORTHANC_ENABLE_LOCALE == 1
+  static std::auto_ptr<std::locale>  globalLocale_;
+
+  static bool SetGlobalLocale(const char* locale)
+  {
+    globalLocale_.reset(NULL);
+
+    try
+    {
+      if (locale == NULL)
+      {
+        LOG(WARNING) << "Falling back to system-wide default locale";
+        globalLocale_.reset(new std::locale());
+      }
+      else
+      {
+        LOG(INFO) << "Using locale: \"" << locale << "\" for case-insensitive comparison of strings";
+        globalLocale_.reset(new std::locale(locale));
+      }
+    }
+    catch (std::runtime_error&)
+    {
+    }
+
+    return (globalLocale_.get() != NULL);
+  }
+  
+  void Toolbox::InitializeGlobalLocale(const char* locale)
+  {
+    // Make Orthanc use English, United States locale
+    // Linux: use "en_US.UTF-8"
+    // Windows: use ""
+    // Wine: use NULL
+    
+#if defined(__MINGW32__)
+    // Visibly, there is no support of locales in MinGW yet
+    // http://mingw.5.n7.nabble.com/How-to-use-std-locale-global-with-MinGW-correct-td33048.html
+    static const char* DEFAULT_LOCALE = NULL;
+#elif defined(_WIN32)
+    // For Windows: use default locale (using "en_US" does not work)
+    static const char* DEFAULT_LOCALE = "";
+#else
+    // For Linux & cie
+    static const char* DEFAULT_LOCALE = "en_US.UTF-8";
+#endif
+
+    bool ok;
+    
+    if (locale == NULL)
+    {
+      ok = SetGlobalLocale(DEFAULT_LOCALE);
+
+#if defined(__MINGW32__)
+      LOG(WARNING) << "This is a MinGW build, case-insensitive comparison of "
+                   << "strings with accents will not work outside of Wine";
+#endif
+    }
+    else
+    {
+      ok = SetGlobalLocale(locale);
+    }
+
+    if (!ok &&
+        !SetGlobalLocale(NULL))
+    {
+      LOG(ERROR) << "Cannot initialize global locale";
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+  }
+
+
+  void Toolbox::FinalizeGlobalLocale()
   {
-#if defined(_WIN32)
-    return static_cast<int>(_getpid());
+    globalLocale_.reset();
+  }
+
+
+  std::string Toolbox::ToUpperCaseWithAccents(const std::string& source)
+  {
+    if (globalLocale_.get() == NULL)
+    {
+      LOG(ERROR) << "No global locale was set, call Toolbox::InitializeGlobalLocale()";
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    /**
+     * A few notes about locales:
+     *
+     * (1) We don't use "case folding":
+     * http://www.boost.org/doc/libs/1_64_0/libs/locale/doc/html/conversions.html
+     *
+     * Characters are made uppercase one by one. This is because, in
+     * static builds, we are using iconv, which is visibly not
+     * supported correctly (TODO: Understand why). Case folding seems
+     * to be working correctly if using the default backend under
+     * Linux (ICU or POSIX?). If one wishes to use case folding, one
+     * would use:
+     *
+     *   boost::locale::generator gen;
+     *   std::locale::global(gen(DEFAULT_LOCALE));
+     *   return boost::locale::to_upper(source);
+     *
+     * (2) The function "boost::algorithm::to_upper_copy" does not
+     * make use of the "std::locale::global()". We therefore create a
+     * global variable "globalLocale_".
+     * 
+     * (3) The variant of "boost::algorithm::to_upper_copy()" that
+     * uses std::string does not work properly. We need to apply it
+     * one wide strings (std::wstring). This explains the two calls to
+     * "utf_to_utf" in order to convert to/from std::wstring.
+     **/
+
+    std::wstring w = boost::locale::conv::utf_to_utf<wchar_t>(source);
+    w = boost::algorithm::to_upper_copy<std::wstring>(w, *globalLocale_);
+    return boost::locale::conv::utf_to_utf<char>(w);
+  }
+#endif
+
+
+  void Toolbox::InitializeOpenSsl()
+  {
+#if ORTHANC_ENABLE_SSL == 1
+    // https://wiki.openssl.org/index.php/Library_Initialization
+    SSL_library_init();
+    SSL_load_error_strings();
+    OpenSSL_add_all_algorithms();
+    ERR_load_crypto_strings();
+#endif
+  }
+
+
+  void Toolbox::FinalizeOpenSsl()
+  {
+#if ORTHANC_ENABLE_SSL == 1
+    // Finalize OpenSSL
+    // https://wiki.openssl.org/index.php/Library_Initialization#Cleanup
+#ifdef FIPS_mode_set
+    FIPS_mode_set(0);
+#endif
+    ENGINE_cleanup();
+    CONF_modules_unload(1);
+    EVP_cleanup();
+    CRYPTO_cleanup_all_ex_data();
+    ERR_remove_state(0);
+    ERR_free_strings();
+#endif
+  }
+
+
+  std::string Toolbox::GenerateUuid()
+  {
+#ifdef WIN32
+    UUID uuid;
+    UuidCreate ( &uuid );
+
+    unsigned char * str;
+    UuidToStringA ( &uuid, &str );
+
+    std::string s( ( char* ) str );
+
+    RpcStringFreeA ( &str );
 #else
-    return static_cast<int>(getpid());
+    uuid_t uuid;
+    uuid_generate_random ( uuid );
+    char s[37];
+    uuid_unparse ( uuid, s );
 #endif
+    return s;
   }
 }
 
+
+
+OrthancLinesIterator* OrthancLinesIterator_Create(const std::string& content)
+{
+  return reinterpret_cast<OrthancLinesIterator*>(new Orthanc::Toolbox::LinesIterator(content));
+}
+
+
+bool OrthancLinesIterator_GetLine(std::string& target,
+                                         const OrthancLinesIterator* iterator)
+{
+  if (iterator != NULL)
+  {
+    return reinterpret_cast<const Orthanc::Toolbox::LinesIterator*>(iterator)->GetLine(target);
+  }
+  else
+  {
+    return false;
+  }
+}
+
+
+void OrthancLinesIterator_Next(OrthancLinesIterator* iterator)
+{
+  if (iterator != NULL)
+  {
+    reinterpret_cast<Orthanc::Toolbox::LinesIterator*>(iterator)->Next();
+  }
+}
+
+
+void OrthancLinesIterator_Free(OrthancLinesIterator* iterator)
+{
+  if (iterator != NULL)
+  {
+    delete reinterpret_cast<const Orthanc::Toolbox::LinesIterator*>(iterator);
+  }
+}
--- a/Core/Toolbox.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Core/Toolbox.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -39,6 +40,35 @@
 #include <string>
 #include <json/json.h>
 
+
+#if !defined(ORTHANC_ENABLE_BASE64)
+#  error The macro ORTHANC_ENABLE_BASE64 must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_LOCALE)
+#  error The macro ORTHANC_ENABLE_LOCALE must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_MD5)
+#  error The macro ORTHANC_ENABLE_MD5 must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_PUGIXML)
+#  error The macro ORTHANC_ENABLE_PUGIXML must be defined
+#endif
+
+
+/**
+ * NOTE: GUID vs. UUID
+ * The simple answer is: no difference, they are the same thing. Treat
+ * them as a 16 byte (128 bits) value that is used as a unique
+ * value. In Microsoft-speak they are called GUIDs, but call them
+ * UUIDs when not using Microsoft-speak.
+ * http://stackoverflow.com/questions/246930/is-there-any-difference-between-a-guid-and-a-uuid
+ **/
+
+
+
 namespace Orthanc
 {
   typedef std::vector<std::string> UriComponents;
@@ -49,10 +79,24 @@
 
   namespace Toolbox
   {
-    void ServerBarrier(const bool& stopFlag);
+    class LinesIterator
+    {
+    private:
+      const std::string& content_;
+      size_t             lineStart_;
+      size_t             lineEnd_;
 
-    void ServerBarrier();
+      void FindEndOfLine();
+  
+    public:
+      LinesIterator(const std::string& content);
+  
+      bool GetLine(std::string& target) const;
 
+      void Next();
+    };
+    
+    
     void ToUpperCase(std::string& s);  // Inplace version
 
     void ToLowerCase(std::string& s);  // Inplace version
@@ -63,20 +107,6 @@
     void ToLowerCase(std::string& result,
                      const std::string& source);
 
-    void ReadFile(std::string& content,
-                  const std::string& path);
-
-    void WriteFile(const std::string& content,
-                   const std::string& path);
-
-    void WriteFile(const void* content,
-                   size_t size,
-                   const std::string& path);
-
-    void USleep(uint64_t microSeconds);
-
-    void RemoveFile(const std::string& path);
-
     void SplitUriComponents(UriComponents& components,
                             const std::string& uri);
   
@@ -92,87 +122,78 @@
     std::string FlattenUri(const UriComponents& components,
                            size_t fromLevel = 0);
 
-    uint64_t GetFileSize(const std::string& path);
-
-#if !defined(ORTHANC_ENABLE_MD5) || ORTHANC_ENABLE_MD5 == 1
+#if ORTHANC_ENABLE_MD5 == 1
     void ComputeMD5(std::string& result,
                     const std::string& data);
 
     void ComputeMD5(std::string& result,
                     const void* data,
-                    size_t length);
+                    size_t size);
 #endif
 
     void ComputeSHA1(std::string& result,
                      const std::string& data);
 
+    void ComputeSHA1(std::string& result,
+                     const void* data,
+                     size_t size);
+
     bool IsSHA1(const char* str,
                 size_t size);
 
     bool IsSHA1(const std::string& s);
 
-#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1
+#if ORTHANC_ENABLE_BASE64 == 1
     void DecodeBase64(std::string& result, 
                       const std::string& data);
 
     void EncodeBase64(std::string& result, 
                       const std::string& data);
 
-#  if BOOST_HAS_REGEX == 1
-    void DecodeDataUriScheme(std::string& mime,
+    bool DecodeDataUriScheme(std::string& mime,
                              std::string& content,
                              const std::string& source);
-#  endif
+
+    void EncodeDataUriScheme(std::string& result,
+                             const std::string& mime,
+                             const std::string& content);
 #endif
 
-    std::string GetPathToExecutable();
-
-    std::string GetDirectoryOfExecutable();
-
+#if ORTHANC_ENABLE_LOCALE == 1
     std::string ConvertToUtf8(const std::string& source,
                               Encoding sourceEncoding);
 
     std::string ConvertFromUtf8(const std::string& source,
                                 Encoding targetEncoding);
+#endif
+
+    bool IsAsciiString(const void* data,
+                       size_t size);
+
+    bool IsAsciiString(const std::string& s);
 
     std::string ConvertToAscii(const std::string& source);
 
     std::string StripSpaces(const std::string& source);
 
-#if BOOST_HAS_DATE_TIME == 1
-    std::string GetNowIsoString();
-
-    void GetNowDicom(std::string& date,
-                     std::string& time);
-#endif
-
     // In-place percent-decoding for URL
     void UrlDecode(std::string& s);
 
     Endianness DetectEndianness();
 
-#if BOOST_HAS_REGEX == 1
     std::string WildcardToRegularExpression(const std::string& s);
-#endif
 
     void TokenizeString(std::vector<std::string>& result,
                         const std::string& source,
                         char separator);
 
-    void MakeDirectory(const std::string& path);
-
-    bool IsExistingFile(const std::string& path);
-
-#if ORTHANC_PUGIXML_ENABLED == 1
+#if ORTHANC_ENABLE_PUGIXML == 1
     void JsonToXml(std::string& target,
                    const Json::Value& source,
                    const std::string& rootElement = "root",
                    const std::string& arrayElement = "item");
 #endif
 
-    void ExecuteSystemCommand(const std::string& command,
-                              const std::vector<std::string>& arguments);
-
     bool IsInteger(const std::string& str);
 
     void CopyJsonWithoutComments(Json::Value& target,
@@ -181,6 +202,62 @@
     bool StartsWith(const std::string& str,
                     const std::string& prefix);
 
-    int GetProcessId();
+    void UriEncode(std::string& target,
+                   const std::string& source);
+
+    std::string GetJsonStringField(const ::Json::Value& json,
+                                   const std::string& key,
+                                   const std::string& defaultValue);
+
+    bool GetJsonBooleanField(const ::Json::Value& json,
+                             const std::string& key,
+                             bool defaultValue);
+
+    int GetJsonIntegerField(const ::Json::Value& json,
+                            const std::string& key,
+                            int defaultValue);
+
+    unsigned int GetJsonUnsignedIntegerField(const ::Json::Value& json,
+                                             const std::string& key,
+                                             unsigned int defaultValue);
+
+    bool IsUuid(const std::string& str);
+
+    bool StartsWithUuid(const std::string& str);
+
+#if ORTHANC_ENABLE_LOCALE == 1
+    void InitializeGlobalLocale(const char* locale);
+
+    void FinalizeGlobalLocale();
+
+    std::string ToUpperCaseWithAccents(const std::string& source);
+#endif
+
+    void InitializeOpenSsl();
+    
+    void FinalizeOpenSsl();
+
+    std::string GenerateUuid();
   }
 }
+
+
+
+
+/**
+ * The plain C, opaque data structure "OrthancLinesIterator" is a thin
+ * wrapper around Orthanc::Toolbox::LinesIterator, and is only used by
+ * "../Resources/WebAssembly/dcdict.cc", in order to avoid code
+ * duplication
+ **/
+
+struct OrthancLinesIterator;
+
+OrthancLinesIterator* OrthancLinesIterator_Create(const std::string& content);
+
+bool OrthancLinesIterator_GetLine(std::string& target,
+                                  const OrthancLinesIterator* iterator);
+
+void OrthancLinesIterator_Next(OrthancLinesIterator* iterator);
+
+void OrthancLinesIterator_Free(OrthancLinesIterator* iterator);
--- a/Core/Uuid.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,162 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "PrecompiledHeaders.h"
-#include "Uuid.h"
-
-// http://stackoverflow.com/a/1626302
-
-extern "C"
-{
-#ifdef WIN32
-#include <rpc.h>
-#else
-#include <uuid/uuid.h>
-#endif
-}
-
-#include <boost/filesystem.hpp>
-
-namespace Orthanc
-{
-  namespace Toolbox
-  {
-    std::string GenerateUuid()
-    {
-#ifdef WIN32
-      UUID uuid;
-      UuidCreate ( &uuid );
-
-      unsigned char * str;
-      UuidToStringA ( &uuid, &str );
-
-      std::string s( ( char* ) str );
-
-      RpcStringFreeA ( &str );
-#else
-      uuid_t uuid;
-      uuid_generate_random ( uuid );
-      char s[37];
-      uuid_unparse ( uuid, s );
-#endif
-      return s;
-    }
-
-
-    bool IsUuid(const std::string& str)
-    {
-      if (str.size() != 36)
-      {
-        return false;
-      }
-
-      for (size_t i = 0; i < str.length(); i++)
-      {
-        if (i == 8 || i == 13 || i == 18 || i == 23)
-        {
-          if (str[i] != '-')
-            return false;
-        }
-        else
-        {
-          if (!isalnum(str[i]))
-            return false;
-        }
-      }
-
-      return true;
-    }
-
-
-    bool StartsWithUuid(const std::string& str)
-    {
-      if (str.size() < 36)
-      {
-        return false;
-      }
-
-      if (str.size() == 36)
-      {
-        return IsUuid(str);
-      }
-
-      assert(str.size() > 36);
-      if (!isspace(str[36]))
-      {
-        return false;
-      }
-
-      return IsUuid(str.substr(0, 36));
-    }
-
-
-    static std::string CreateTemporaryPath(const char* extension)
-    {
-#if BOOST_HAS_FILESYSTEM_V3 == 1
-      boost::filesystem::path tmpDir = boost::filesystem::temp_directory_path();
-#elif defined(__linux__)
-      boost::filesystem::path tmpDir("/tmp");
-#else
-#error Support your platform here
-#endif
-
-      // We use UUID to create unique path to temporary files
-      std::string filename = "Orthanc-" + Orthanc::Toolbox::GenerateUuid();
-
-      if (extension != NULL)
-      {
-        filename.append(extension);
-      }
-
-      tmpDir /= filename;
-      return tmpDir.string();
-    }
-
-
-    TemporaryFile::TemporaryFile() : 
-      path_(CreateTemporaryPath(NULL))
-    {
-    }
-
-
-    TemporaryFile::TemporaryFile(const char* extension) :
-      path_(CreateTemporaryPath(extension))
-    {
-    }
-
-
-    TemporaryFile::~TemporaryFile()
-    {
-      boost::filesystem::remove(path_);
-    }  
-  }
-}
--- a/Core/Uuid.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,86 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include <string>
-
-/**
- * GUID vs. UUID
- * The simple answer is: no difference, they are the same thing. Treat
- * them as a 16 byte (128 bits) value that is used as a unique
- * value. In Microsoft-speak they are called GUIDs, but call them
- * UUIDs when not using Microsoft-speak.
- * http://stackoverflow.com/questions/246930/is-there-any-difference-between-a-guid-and-a-uuid
- **/
-
-#include "Toolbox.h"
-
-namespace Orthanc
-{
-  namespace Toolbox
-  {
-    std::string GenerateUuid();
-
-    bool IsUuid(const std::string& str);
-
-    bool StartsWithUuid(const std::string& str);
-
-    class TemporaryFile
-    {
-    private:
-      std::string path_;
-
-    public:
-      TemporaryFile();
-
-      TemporaryFile(const char* extension);
-
-      ~TemporaryFile();
-
-      const std::string& GetPath() const
-      {
-        return path_;
-      }
-
-      void Write(const std::string& content)
-      {
-        Toolbox::WriteFile(content, path_);
-      }
-
-      void Read(std::string& content) const
-      {
-        Toolbox::ReadFile(content, path_);
-      }
-    };
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/WebServiceParameters.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,504 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeaders.h"
+#include "WebServiceParameters.h"
+
+#include "Logging.h"
+#include "OrthancException.h"
+#include "SerializationToolbox.h"
+#include "Toolbox.h"
+
+#if ORTHANC_SANDBOXED == 0
+#  include "SystemToolbox.h"
+#endif
+
+#include <cassert>
+
+namespace Orthanc
+{
+  static const char* KEY_CERTIFICATE_FILE = "CertificateFile";
+  static const char* KEY_CERTIFICATE_KEY_FILE = "CertificateKeyFile";
+  static const char* KEY_CERTIFICATE_KEY_PASSWORD = "CertificateKeyPassword";
+  static const char* KEY_HTTP_HEADERS = "HttpHeaders";
+  static const char* KEY_PASSWORD = "Password";
+  static const char* KEY_PKCS11 = "Pkcs11";
+  static const char* KEY_URL = "Url";
+  static const char* KEY_URL_2 = "URL";
+  static const char* KEY_USERNAME = "Username";
+
+
+  static bool IsReservedKey(const std::string& key)
+  {
+    return (key == KEY_CERTIFICATE_FILE ||
+            key == KEY_CERTIFICATE_KEY_FILE ||
+            key == KEY_CERTIFICATE_KEY_PASSWORD ||
+            key == KEY_HTTP_HEADERS ||
+            key == KEY_PASSWORD ||
+            key == KEY_PKCS11 ||
+            key == KEY_URL ||
+            key == KEY_URL_2 ||
+            key == KEY_USERNAME);
+  }
+
+
+  WebServiceParameters::WebServiceParameters() : 
+    pkcs11Enabled_(false)
+  {
+    SetUrl("http://127.0.0.1:8042/");
+  }
+
+
+  void WebServiceParameters::ClearClientCertificate()
+  {
+    certificateFile_.clear();
+    certificateKeyFile_.clear();
+    certificateKeyPassword_.clear();
+  }
+
+
+  void WebServiceParameters::SetUrl(const std::string& url)
+  {
+    if (!Toolbox::StartsWith(url, "http://") &&
+        !Toolbox::StartsWith(url, "https://"))
+    {
+      LOG(ERROR) << "Bad URL: " << url;
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    // Add trailing slash if needed
+    if (url[url.size() - 1] == '/')
+    {
+      url_ = url;
+    }
+    else
+    {
+      url_ = url + '/';
+    }
+  }
+
+
+  void WebServiceParameters::ClearCredentials()
+  {
+    username_.clear();
+    password_.clear();
+  }
+
+
+  void WebServiceParameters::SetCredentials(const std::string& username,
+                                            const std::string& password)
+  {
+    if (username.empty() && 
+        !password.empty())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      username_ = username;
+      password_ = password;
+    }
+  }
+
+
+  void WebServiceParameters::SetClientCertificate(const std::string& certificateFile,
+                                                  const std::string& certificateKeyFile,
+                                                  const std::string& certificateKeyPassword)
+  {
+    if (certificateFile.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (certificateKeyPassword.empty())
+    {
+      LOG(ERROR) << "The password for the HTTPS certificate is not provided: " << certificateFile;
+      throw OrthancException(ErrorCode_BadFileFormat);      
+    }
+
+    certificateFile_ = certificateFile;
+    certificateKeyFile_ = certificateKeyFile;
+    certificateKeyPassword_ = certificateKeyPassword;
+  }
+
+
+  void WebServiceParameters::FromSimpleFormat(const Json::Value& peer)
+  {
+    assert(peer.isArray());
+
+    pkcs11Enabled_ = false;
+    ClearClientCertificate();
+
+    if (peer.size() != 1 && 
+        peer.size() != 3)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    SetUrl(peer.get(0u, "").asString());
+
+    if (peer.size() == 1)
+    {
+      ClearCredentials();
+    }
+    else if (peer.size() == 2)
+    {
+      LOG(ERROR) << "The HTTP password is not provided";
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    else if (peer.size() == 3)
+    {
+      SetCredentials(peer.get(1u, "").asString(),
+                     peer.get(2u, "").asString());
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  static std::string GetStringMember(const Json::Value& peer,
+                                     const std::string& key,
+                                     const std::string& defaultValue)
+  {
+    if (!peer.isMember(key))
+    {
+      return defaultValue;
+    }
+    else if (peer[key].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      return peer[key].asString();
+    }
+  }
+
+
+  void WebServiceParameters::FromAdvancedFormat(const Json::Value& peer)
+  {
+    assert(peer.isObject());
+
+    std::string url = GetStringMember(peer, KEY_URL, "");
+    if (url.empty())
+    {
+      SetUrl(GetStringMember(peer, KEY_URL_2, ""));
+    }
+    else
+    {
+      SetUrl(url);
+    }
+
+    SetCredentials(GetStringMember(peer, KEY_USERNAME, ""),
+                   GetStringMember(peer, KEY_PASSWORD, ""));
+
+    std::string file = GetStringMember(peer, KEY_CERTIFICATE_FILE, "");
+    if (!file.empty())
+    {
+      SetClientCertificate(file, GetStringMember(peer, KEY_CERTIFICATE_KEY_FILE, ""),
+                           GetStringMember(peer, KEY_CERTIFICATE_KEY_PASSWORD, ""));
+    }
+    else
+    {
+      ClearClientCertificate();
+    }
+
+    if (peer.isMember(KEY_PKCS11))
+    {
+      if (peer[KEY_PKCS11].type() == Json::booleanValue)
+      {
+        pkcs11Enabled_ = peer[KEY_PKCS11].asBool();
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+    else
+    {
+      pkcs11Enabled_ = false;
+    }
+
+
+    headers_.clear();
+
+    if (peer.isMember(KEY_HTTP_HEADERS))
+    {
+      const Json::Value& h = peer[KEY_HTTP_HEADERS];
+      if (h.type() != Json::objectValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        Json::Value::Members keys = h.getMemberNames();
+        for (size_t i = 0; i < keys.size(); i++)
+        {
+          const Json::Value& value = h[keys[i]];
+          if (value.type() != Json::stringValue)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+          else
+          {
+            headers_[keys[i]] = value.asString();
+          }
+        }
+      }
+    }
+
+
+    userProperties_.clear();
+
+    const Json::Value::Members members = peer.getMemberNames();
+
+    for (Json::Value::Members::const_iterator it = members.begin(); 
+         it != members.end(); ++it)
+    {
+      if (!IsReservedKey(*it))
+      {
+        if (peer[*it].type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          userProperties_[*it] = peer[*it].asString();
+        }
+      }
+    }
+  }
+
+
+  void WebServiceParameters::Unserialize(const Json::Value& peer)
+  {
+    try
+    {
+      if (peer.isArray())
+      {
+        FromSimpleFormat(peer);
+      }
+      else if (peer.isObject())
+      {
+        FromAdvancedFormat(peer);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+    catch (OrthancException&)
+    {
+      throw;
+    }
+    catch (...)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  void WebServiceParameters::ListHttpHeaders(std::set<std::string>& target) const
+  {
+    target.clear();
+
+    for (Dictionary::const_iterator it = headers_.begin();
+         it != headers_.end(); ++it)
+    {
+      target.insert(it->first);
+    }
+  }
+
+
+  bool WebServiceParameters::LookupHttpHeader(std::string& value,
+                                              const std::string& key) const
+  {
+    Dictionary::const_iterator found = headers_.find(key);
+
+    if (found == headers_.end())
+    {
+      return false;
+    }
+    else
+    {
+      value = found->second;
+      return true;
+    }
+  }
+
+
+  void WebServiceParameters::AddUserProperty(const std::string& key,
+                                             const std::string& value)
+  {
+    if (IsReservedKey(key))
+    {
+      LOG(ERROR) << "Cannot use this reserved key to name an user property: " << key;
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      userProperties_[key] = value;
+    }
+  }
+
+
+  void WebServiceParameters::ListUserProperties(std::set<std::string>& target) const
+  {
+    target.clear();
+
+    for (Dictionary::const_iterator it = userProperties_.begin();
+         it != userProperties_.end(); ++it)
+    {
+      target.insert(it->first);
+    }
+  }
+
+
+  bool WebServiceParameters::LookupUserProperty(std::string& value,
+                                                const std::string& key) const
+  {
+    Dictionary::const_iterator found = userProperties_.find(key);
+
+    if (found == userProperties_.end())
+    {
+      return false;
+    }
+    else
+    {
+      value = found->second;
+      return true;
+    }
+  }
+
+
+  bool WebServiceParameters::IsAdvancedFormatNeeded() const
+  {
+    return (!certificateFile_.empty() ||
+            !certificateKeyFile_.empty() ||
+            !certificateKeyPassword_.empty() ||
+            pkcs11Enabled_ ||
+            !headers_.empty() ||
+            !userProperties_.empty());
+  }
+
+
+  void WebServiceParameters::Serialize(Json::Value& value,
+                                       bool forceAdvancedFormat,
+                                       bool includePasswords) const
+  {
+    if (forceAdvancedFormat ||
+        IsAdvancedFormatNeeded())
+    {
+      value = Json::objectValue;
+      value[KEY_URL] = url_;
+
+      if (!username_.empty() ||
+          !password_.empty())
+      {
+        value[KEY_USERNAME] = username_;
+
+        if (includePasswords)
+        {
+          value[KEY_PASSWORD] = password_;
+        }
+      }
+
+      if (!certificateFile_.empty())
+      {
+        value[KEY_CERTIFICATE_FILE] = certificateFile_;
+      }
+
+      if (!certificateKeyFile_.empty())
+      {
+        value[KEY_CERTIFICATE_KEY_FILE] = certificateKeyFile_;
+      }
+
+      if (!certificateKeyPassword_.empty() &&
+          includePasswords)
+      {
+        value[KEY_CERTIFICATE_KEY_PASSWORD] = certificateKeyPassword_;
+      }
+
+      value[KEY_PKCS11] = pkcs11Enabled_;
+
+      value[KEY_HTTP_HEADERS] = Json::objectValue;
+      for (Dictionary::const_iterator it = headers_.begin();
+           it != headers_.end(); ++it)
+      {
+        value[KEY_HTTP_HEADERS][it->first] = it->second;
+      }
+
+      for (Dictionary::const_iterator it = userProperties_.begin();
+           it != userProperties_.end(); ++it)
+      {
+        value[it->first] = it->second;
+      }
+    }
+    else
+    {
+      value = Json::arrayValue;
+      value.append(url_);
+
+      if (!username_.empty() ||
+          !password_.empty())
+      {
+        value.append(username_);
+        value.append(includePasswords ? password_ : "");
+      }
+    }
+  }
+
+
+#if ORTHANC_SANDBOXED == 0
+  void WebServiceParameters::CheckClientCertificate() const
+  {
+    if (!certificateFile_.empty())
+    {
+      if (!SystemToolbox::IsRegularFile(certificateFile_))
+      {
+        LOG(ERROR) << "Cannot open certificate file: " << certificateFile_;
+        throw OrthancException(ErrorCode_InexistentFile);
+      }
+
+      if (!certificateKeyFile_.empty() && 
+          !SystemToolbox::IsRegularFile(certificateKeyFile_))
+      {
+        LOG(ERROR) << "Cannot open key file: " << certificateKeyFile_;
+        throw OrthancException(ErrorCode_InexistentFile);
+      }
+    }
+  }
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/WebServiceParameters.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,179 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#include <map>
+#include <set>
+#include <string>
+#include <json/json.h>
+
+namespace Orthanc
+{
+  class WebServiceParameters
+  {
+  public:
+    typedef std::map<std::string, std::string>  Dictionary;
+
+  private:
+    std::string  url_;
+    std::string  username_;
+    std::string  password_;
+    std::string  certificateFile_;
+    std::string  certificateKeyFile_;
+    std::string  certificateKeyPassword_;
+    bool         pkcs11Enabled_;
+    Dictionary   headers_;
+    Dictionary   userProperties_;
+
+    void FromSimpleFormat(const Json::Value& peer);
+
+    void FromAdvancedFormat(const Json::Value& peer);
+
+  public:
+    WebServiceParameters();
+
+    WebServiceParameters(const Json::Value& serialized)
+    {
+      Unserialize(serialized);
+    }
+
+    const std::string& GetUrl() const
+    {
+      return url_;
+    }
+
+    void SetUrl(const std::string& url);
+
+    void ClearCredentials();
+
+    void SetCredentials(const std::string& username,
+                        const std::string& password);
+    
+    const std::string& GetUsername() const
+    {
+      return username_;
+    }
+
+    const std::string& GetPassword() const
+    {
+      return password_;
+    }
+
+    void ClearClientCertificate();
+
+    void SetClientCertificate(const std::string& certificateFile,
+                              const std::string& certificateKeyFile,
+                              const std::string& certificateKeyPassword);
+
+    const std::string& GetCertificateFile() const
+    {
+      return certificateFile_;
+    }
+
+    const std::string& GetCertificateKeyFile() const
+    {
+      return certificateKeyFile_;
+    }
+
+    const std::string& GetCertificateKeyPassword() const
+    {
+      return certificateKeyPassword_;
+    }
+
+    void SetPkcs11Enabled(bool enabled)
+    {
+      pkcs11Enabled_ = enabled;
+    }
+
+    bool IsPkcs11Enabled() const
+    {
+      return pkcs11Enabled_;
+    }
+
+    void AddHttpHeader(const std::string& key,
+                       const std::string& value)
+    {
+      headers_[key] = value;
+    }
+
+    void ClearHttpHeaders()
+    {
+      headers_.clear();
+    }
+
+    const Dictionary& GetHttpHeaders() const
+    {
+      return headers_;
+    }
+
+    void ListHttpHeaders(std::set<std::string>& target) const; 
+
+    bool LookupHttpHeader(std::string& value,
+                          const std::string& key) const; 
+
+    void AddUserProperty(const std::string& key,
+                         const std::string& value);
+
+    void ClearUserProperties()
+    {
+      userProperties_.clear();
+    }
+
+    const Dictionary& GetUserProperties() const
+    {
+      return userProperties_;
+    }
+
+    void ListUserProperties(std::set<std::string>& target) const; 
+
+    bool LookupUserProperty(std::string& value,
+                            const std::string& key) const; 
+
+    bool IsAdvancedFormatNeeded() const;
+
+    void Unserialize(const Json::Value& peer);
+
+    void Serialize(Json::Value& value,
+                   bool forceAdvancedFormat,
+                   bool includePasswords) const;
+
+#if ORTHANC_SANDBOXED == 0
+    void CheckClientCertificate() const;
+#endif
+  };
+}
--- a/DarwinCompilation.txt	Thu Oct 29 11:25:45 2015 +0100
+++ b/DarwinCompilation.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -31,7 +31,9 @@
 # cmake -GXcode -DCMAKE_OSX_DEPLOYMENT_TARGET=10.8 -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON ~/Orthanc
 
 NB: Adapt the value of "CMAKE_OSX_DEPLOYMENT_TARGET" with respect to
-your version of XCode.
+your version of OS X. This version can obtained by typing:
+
+# sw_vers
 
 
 Build the Debug version of Orthanc
@@ -47,6 +49,6 @@
 ------------------------------------
 
 # xcodebuild -configuration Release
-# ./Debug/UnitTests
+# ./Release/UnitTests
 
 The binaries of Orthanc are located at "~/OrthancBuild/Release/Orthanc".
--- a/INSTALL	Thu Oct 29 11:25:45 2015 +0100
+++ b/INSTALL	Fri Oct 12 15:18:10 2018 +0200
@@ -51,14 +51,19 @@
 third-party dependencies, delete the "~/Orthanc/ThirdPartyDownloads/"
 folder, then restart cmake.
 
+WARNING 3: If performance is important to you, make sure to add the
+option "-DCMAKE_BUILD_TYPE=Release" when invoking cmake. Indeed, by
+default, run-time debug assertions are enabled, which can seriously
+impact performance, especially if your Orthanc server stores a lot of
+DICOM instances.
 
-Native Linux Compilation
-------------------------
+
+Native GNU/Linux Compilation
+----------------------------
 
 See the file "LinuxCompilation.txt".
 
 
-
 Native OS X Compilation
 -----------------------
 
@@ -70,36 +75,55 @@
 -------------------------------------------------
 
 # cd [...]\OrthancBuild
-# cmake -DSTANDALONE_BUILD=ON -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON -G "Visual Studio 8 2005" [...]\Orthanc
+# cmake -DSTANDALONE_BUILD=ON -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON -DUSE_LEGACY_JSONCPP=ON -G "Visual Studio 9 2008" [...]\Orthanc
 
 Then open the "[...]/OrthancBuild/Orthanc.sln" with Visual Studio.
 
 NOTES:
-* More recent versions of Visual Studio than 2005 should also
+* More recent versions of Visual Studio than 2008 should also
   work. Type "cmake" without arguments to have the list of generators
   that are available on your computer.
 * You will have to install the Platform SDK (version 6 or above) for
   Visual Studio 2005:
   http://en.wikipedia.org/wiki/Microsoft_Windows_SDK.
   Read the CMake FAQ: http://goo.gl/By90B 
+* The "-DUSE_LEGACY_JSONCPP=ON" must be set for versions of
+  Visual Studio that do not support C++11
 
 
 
-Cross-Compilation for Windows under Linux
------------------------------------------
+Cross-Compilation for Windows under GNU/Linux
+---------------------------------------------
 
-To cross-compile Windows binaries under Linux using MinGW, please use
-the following command:
+Some versions of MinGW-W64 might have problems with C++11 (notably
+those shipped in Ubuntu 16.04 LTS, in the "mingw-w64" package). Use
+the following command to disable C++11:
 
 # cd ~/OrthancBuild
-# cmake -DCMAKE_TOOLCHAIN_FILE=~/Orthanc/Resources/MinGWToolchain.cmake -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DCMAKE_BUILD_TYPE=Debug ~/Orthanc
+# cmake ~/Orthanc \
+        -DCMAKE_BUILD_TYPE=Debug \
+        -DCMAKE_TOOLCHAIN_FILE=~/Orthanc/Resources/MinGW-W64-Toolchain32.cmake \
+        -DSTANDALONE_BUILD=ON \
+        -DSTATIC_BUILD=ON \
+        -DUSE_LEGACY_JSONCPP=ON
 # make
 
+NB: Use the toolchain "MinGW-W64-Toolchain64.cmake" to produce 64bit
+Windows binaries.
+
 
 
-Native Windows build with MinGW (VERY SLOW)
--------------------------------------------
+Legacy MinGW32 compilers (notably those shipped in Ubuntu 14.04 LTS,
+in the "mingw32" package) are incompatible with DCMTK 3.6.2 and
+C++11. Use the following command to force using DCMTK 3.6.0 and
+disable C++11:
 
-# cd [...]\OrthancBuild
-# cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Debug [...]\Orthanc
-# mingw32-make
+# cd ~/OrthancBuild
+# cmake ~/Orthanc \
+        -DCMAKE_BUILD_TYPE=Debug \
+        -DCMAKE_TOOLCHAIN_FILE=~/Orthanc/Resources/MinGWToolchain.cmake \
+        -DSTANDALONE_BUILD=ON \
+        -DSTATIC_BUILD=ON \
+        -DUSE_DCMTK_360=ON \
+        -DUSE_LEGACY_JSONCPP=ON
+# make
--- a/LinuxCompilation.txt	Thu Oct 29 11:25:45 2015 +0100
+++ b/LinuxCompilation.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -1,17 +1,17 @@
 This file is a complement to "INSTALL", which contains instructions
-that are specific to Linux.
+that are specific to GNU/Linux.
 
 
-Static linking for Linux
-========================
+Static linking for GNU/Linux
+============================
 
-The most simple way of building Orthanc under Linux consists in
+The most simple way of building Orthanc under GNU/Linux consists in
 statically linking against all the third-party dependencies. In this
 case, the system-wide libraries will not be used. The build tool
 (CMake) will download the sources of all the required packages and
 automatically compile them.
 
-This process should work on any Linux distribution, provided that a
+This process should work on any GNU/Linux distribution, provided that a
 C/C++ compiler ("build-essential" in Debian-based systems), the Python
 interpreter, CMake, the "unzip" system tool, and the development
 package for libuuid ("uuid-dev" in Debian) are installed.
@@ -47,14 +47,14 @@
 Note 3- To build the documentation, you will have to install doxyen.
 
 
-Use system-wide libraries under Linux
-=====================================
+Use system-wide libraries under GNU/Linux
+=========================================
 
-Under Linux, by default, Orthanc links against the shared libraries of
-your system (the "STATIC_BUILD" option is set to "OFF"). This greatly
-speeds up the compilation. This is also required when building
-packages for Linux distributions. Because using system libraries is
-the default behavior, you just have to use:
+Under GNU/Linux, by default, Orthanc links against the shared
+libraries of your system (the "STATIC_BUILD" option is set to
+"OFF"). This greatly speeds up the compilation. This is also required
+when building packages for GNU/Linux distributions. Because using
+system libraries is the default behavior, you just have to use:
 
 # cd ~/OrthancBuild
 # cmake -DCMAKE_BUILD_TYPE=Debug ~/Orthanc
@@ -62,13 +62,14 @@
 
 Note that to build the documentation, you will have to install doxyen.
 
-However, on some Linux distributions, it is still required to download
-and static link against some third-party dependencies, e.g. when the
-system-wide library is not shipped or is outdated. Because of
-difference in the packaging of the various Linux distribution, it is
-also sometimes required to fine-tune some options.
+However, on some GNU/Linux distributions, it is still required to
+download and static link against some third-party dependencies,
+e.g. when the system-wide library is not shipped or is
+outdated. Because of difference in the packaging of the various
+GNU/Linux distribution, it is also sometimes required to fine-tune
+some options.
 
-You will find below build instructions for specific Linux
+You will find below build instructions for specific GNU/Linux
 distributions. Distributions tagged by "SUPPORTED" are tested by
 Sébastien Jodogne. Distributions tagged by "CONTRIBUTED" come from
 Orthanc users.
@@ -79,14 +80,15 @@
 
 # sudo apt-get install build-essential unzip cmake mercurial \
        	       	       uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \
-       	       	       libgoogle-glog-dev libgtest-dev libpng-dev libjpeg-dev \
+       	       	       libgtest-dev libpng-dev libjpeg-dev \
        	       	       libsqlite3-dev libssl-dev zlib1g-dev libdcmtk2-dev \
                        libboost-all-dev libwrap0-dev libjsoncpp-dev libpugixml-dev
 
 # cmake -DALLOW_DOWNLOADS=ON \
 	-DUSE_SYSTEM_MONGOOSE=OFF \
-        -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
+        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
         -DDCMTK_LIBRARIES=dcmjpls \
+        -DCMAKE_BUILD_TYPE=Release \
 	~/Orthanc
 
 Note: Have also a look at the official package:
@@ -106,9 +108,9 @@
         -DALLOW_DOWNLOADS=ON \
 	-DUSE_SYSTEM_MONGOOSE=OFF \
 	-DUSE_SYSTEM_JSONCPP=OFF \
-	-DUSE_SYSTEM_GOOGLE_LOG=OFF \
 	-DUSE_SYSTEM_PUGIXML=OFF \
-        -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
+        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
+        -DCMAKE_BUILD_TYPE=Release \
 	~/Orthanc
 
 
@@ -123,9 +125,10 @@
                        libcharls-dev libjsoncpp-dev libpugixml-dev
 
 # cmake -DALLOW_DOWNLOADS=ON \
-        -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
+        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
         -DUSE_SYSTEM_MONGOOSE=OFF \
         -DDCMTK_LIBRARIES=dcmjpls \
+        -DCMAKE_BUILD_TYPE=Release \
         ~/Orthanc
 
 
@@ -134,7 +137,7 @@
 ------------------------
 
 # sudo yum install unzip make automake gcc gcc-c++ python cmake \
-                   boost-devel curl-devel dcmtk-devel glog-devel \
+                   boost-devel curl-devel dcmtk-devel \
                    gtest-devel libpng-devel libsqlite3x-devel libuuid-devel jpeg-devel \
                    mongoose-devel openssl-devel jsoncpp-devel lua-devel pugixml-devel
 
@@ -144,6 +147,7 @@
 
 # cmake  "-DDCMTK_LIBRARIES=CharLS" \
          -DSYSTEM_MONGOOSE_USE_CALLBACKS=OFF \
+         -DCMAKE_BUILD_TYPE=Release \
          ~/Orthanc
        
 Note: Have also a look at the official package:
@@ -155,11 +159,12 @@
 ------------------------
 
 # pkg install jsoncpp pugixml lua51 curl googletest dcmtk cmake jpeg \
-              e2fsprogs-libuuid glog boost-libs sqlite3 python libiconv
+              e2fsprogs-libuuid boost-libs sqlite3 python libiconv
 
 # cmake -DALLOW_DOWNLOADS=ON \
         -DUSE_SYSTEM_MONGOOSE=OFF \
         -DDCMTK_LIBRARIES="dcmdsig;charls;dcmjpls" \
+        -DCMAKE_BUILD_TYPE=Release \
 	~/Orthanc
 
 
@@ -180,12 +185,13 @@
         -DUSE_SYSTEM_DCMTK=OFF \
         -DUSE_SYSTEM_GOOGLE_TEST=OFF \
         -DUSE_SYSTEM_LIBJPEG=OFF \
+        -DCMAKE_BUILD_TYPE=Release \
         ~/Orthanc
 
 
 
-Other Linux distributions?
---------------------------
+Other GNU/Linux distributions?
+------------------------------
 
 Please send us your build instructions (by a mail to
 s.jodogne@gmail.com)!
@@ -203,7 +209,7 @@
 Using ccache
 ============
 
-Under Linux, you also have the opportunity to use "ccache" to
+Under GNU/Linux, you also have the opportunity to use "ccache" to
 dramatically decrease the compilation time when rebuilding
 Orthanc. This is especially useful for developers. To this end, you
 would use:
--- a/NEWS	Thu Oct 29 11:25:45 2015 +0100
+++ b/NEWS	Fri Oct 12 15:18:10 2018 +0200
@@ -1,21 +1,492 @@
 Pending changes in the mainline
 ===============================
 
-* Full indexation of the patient/study tags to speed up searches and C-Find
-* Add ".dcm" suffix to files in ZIP archives (cf. URI ".../archive")
-* "/tools/create-dicom": Support of binary tags encoded using data URI scheme
-* "/tools/create-dicom": Support of hierarchical structures (creation of sequences)
-* "/modify" can insert/modify sequences
-* "/series/.../ordered-slices" to order the slices of a 2D+t or 3D image
-* New URI "/tools/shutdown" to stop Orthanc from the REST API
-* New URIs for attachments: ".../compress", ".../uncompress" and ".../is-compressed"
+
+General
+-------
+
+* Possibility to restrict the allowed DICOM commands for each modality
+
+Orthanc Explorer
+----------------
+
+* The first screen of Orthanc Explorer is now a form to do studies lookups
+* Support of large databases, by limiting the results to 100 patients or studies
+
+REST API
+--------
+
+* New URI: "/studies/.../merge" to merge a study
+* New URI: "/studies/.../split" to split a study
+
+Maintenance
+-----------
+
+* Executing a query/retrieve from the REST API now creates a job
+* Fix: Closing DICOM associations after running query/retrieve from REST API
+
+
+Version 1.4.2 (2018-09-20)
+==========================
+
+General
+-------
+
+* "OrthancPeers" configuration option now allows to specify HTTP headers
+* New main DICOM tag: "ImageOrientationPatient" at the instance level
+* New configuration options:
+  - "HttpVerbose" to debug outgoing HTTP connections
+  - "OverwriteInstances" to choose how duplicate SOPInstanceUID are handled
+
+Orthanc Explorer
+----------------
+
+* Query/retrieve: Added button for "DX" modality
+
+REST API
+--------
+
+* "/tools/reconstruct" to reconstruct the main DICOM tags, the JSON summary and
+  the metadata of all the instances stored in Orthanc. This is a slow operation!
+
+Plugins
+-------
+
+* New primitives to access Orthanc peers from plugins
+* New events in change callbacks: "UpdatedPeers" and "UpdatedModalities"
+* New primitives to handle jobs from plugins: "OrthancPluginSubmitJob()"
+  and "OrthancPluginRegisterJobsUnserializer()"
+
+Lua
+---
+
+* IncomingWorklistRequestFilter() to filter incoming C-FIND worklist queries
+
+Maintenance
+-----------
+
+* Fix "/series/.../ordered-slices" in the presence of non-parallel slices
+* Fix incoming DICOM C-Store filtering for JPEG-LS transfer syntaxes
+* Fix OrthancPluginHttpClient() to return the HTTP status on errors
+* Fix HTTPS requests to sites using a certificate encrypted with ECDSA
+* Fix handling of incoming C-FIND queries containing Generic Group Length (*, 0x0000)
+* Fix issue 54 (quoting multipart answers), for OsiriX compatibility through DICOMweb
+* Fix issue 98 (DCMTK configuration fails with GCC 6.4.0 on Alpine)
+* Fix issue 99 (PamWriter test segfaults on alpine linux with gcc 6.4.0)
+
+
+Version 1.4.1 (2018-07-17)
+==========================
+
+* Fix deadlock in Lua scripting
+* Simplification to the "DatabaseWrapper" class
+
+
+Version 1.4.0 (2018-07-13)
+==========================
+
+General
+-------
+
+* New advanced job engine
+* New configuration options:
+  - "ConcurrentJobs": Max number of jobs that are simultaneously running
+  - "SynchronousCMove": Whether to run DICOM C-Move operations synchronously
+  - "JobsHistorySize": Max number of completed jobs that are kept in memory
+* New metadata automatically computed at the instance level: 
+  "RemoteIp", "CalledAet" and "HttpUsername"
+
+Orthanc Explorer
+----------------
+
+* New screen listing all the available studies
+
+REST API
+--------
+
+* "/jobs/..." to manage the jobs from the REST API
+* New option "?short" to list DICOM tags using their hexadecimal ID in:
+  - "/instances/.../tags?short"
+  - "/instances/.../header?short"
+  - "/{patients|studies|series}/.../instances-tags?short"
+  - "/{patients|studies|series}/.../shared-tags?short"
+  - "/{patients|studies|series|instances}/.../module?short"
+  - "/studies/.../module-patient?short"
+* "/instances/.../tags" URI was returning only the first value of
+  DicomTags containing multiple numerical value.  It now returns all
+  values in a string separated by \\ (i.e.: "1\\2\\3").  Note that,
+  for data already in Orthanc, you'll need to reconstruct the data by
+  sending a POST request to the ".../reconstruct" URI.  This change
+  triggered an update of ORTHANC_API_VERSION from 1.0 to 1.1
+* "/instances/.../frame/../image-uint8 and friends now accepts a 
+  "image/pam" MIME type to retrieve images in PAM format
+  (https://en.wikipedia.org/wiki/Netpbm#PAM_graphics_format)
+* New option "?expand" to "/instances/.../metadata"
+
+Plugins
+-------
+
+* New primitive in database SDK: "lookupIdentifierRange" to speed up range searches
+* New function in the SDK: "OrthancPluginCheckVersionAdvanced()"
+
+Maintenance
+-----------
+
+* Configuration option "LogExportedResources" is now "false" by default
+* Header "OrthancCppDatabasePlugin.h" is now part of the "orthanc-databases" project
+* Fix generation of DICOMDIR if PatientID is empty
+* Fix issue 25 (Deadlock with Lua scripts): The event queue is now implemented for Lua
+* Fix issue 94 (Instance modification should not modify FrameOfReferenceUID)
+* Fix issue 77 (Lua access to REST-API is null terminated)
+* Fix memory leak introduced by changeset #99116ed6f38c in Orthanc 1.3.2
+* Upgraded dependencies for static and Windows builds:
+  - boost 1.67.0
+  - openssl 1.0.2o
+
+
+Version 1.3.2 (2018-04-18)
+==========================
+
+REST API
+--------
+
+* "/system" URI returns the version of the Orthanc REST API
+* "/tools/now" returns the current UTC (universal) time
+* "/tools/now-local" returns the curent local time.
+  This was the behavior of "/tools/now" until release 1.3.1.
+* Added "?expand" GET argument to "/peers" and "/modalities" routes
+* New URI: "/tools/create-media-extended" to generate a DICOMDIR
+  archive from several resources, including additional type-3 tags
+* Preservation of UID relationships while anonymizing
+
+Lua
+---
+
+* New CMake option: "-DENABLE_LUA_MODULES=ON" to enable support for
+  loading external Lua modules if the Lua engine is statically linked
+
+Plugins
+-------
+
+* New error code: DatabaseUnavailable
+
+Maintenance
+-----------
+
+* Orthanc now uses UTC (universal time) instead of local time in its database
+* Fix to allow creating DICOM instances with empty Specific Character Set (0008,0005)
+* Support of Linux Standard Base
+* Static linking against libuuid (from e2fsprogs)
+* Fix static build on CentOS 6
+* Possibility of using JsonCpp 0.10.6 if the compiler does not support C++11
+  with the "-DUSE_LEGACY_JSONCPP=ON" CMake option
+* Upgraded dependencies for static and Windows builds:
+  - boost 1.66.0
+  - curl 7.57.0
+  - jsoncpp 1.8.4
+  - zlib 1.2.11
+
+
+Version 1.3.1 (2017-11-29)
+==========================
+
+General
+-------
+
+* Built-in decoding of palette images
+
+REST API
+--------
+
+* New URI: "/instances/.../frames/.../raw.gz" to compress raw frames using gzip
+* New argument "ignore-length" to force the inclusion of too long tags in JSON
+* New argument "/.../media?extended" to include additional type-3 tags in DICOMDIR
 
 Plugins
 -------
 
-* New function "OrthancPluginRegisterErrorCode()" to declare custom error codes
-* New function "OrthancPluginRegisterDictionaryTag()" to declare DICOM tags
-* New "OrthancStarted" and "OrthancStopped" events in change callbacks
+* New pixel formats exposed in SDK: BGRA32, Float32, Grayscale32, RGB48
+
+Maintenance
+-----------
+
+* Creation of ./Resources/CMake/OrthancFramework*.cmake to reuse the Orthanc
+  C++ framework in other projects
+* New security-related options: "DicomAlwaysAllowEcho"
+* Use "GBK" (frequently used in China) as an alias for "GB18030"
+* Experimental support of actively maintained Civetweb to replace Mongoose 3.8
+* Fix issue 31 for good (create new modality types for Philips ADW, GE Xeleris, GE AWServer)
+* Fix issue 64 (OpenBSD support)
+* Fix static compilation of DCMTK 3.6.2 on Fedora
+* Upgrade to Boost 1.65.1 in static builds
+* Upgrade to SQLite amalgamation 3.21.0 in static builds
+
+
+Version 1.3.0 (2017-07-19)
+==========================
+
+General
+-------
+
+* Orthanc now anonymizes according to Basic Profile of PS 3.15-2017c Table E.1-1
+* In the "DicomModalities" configuration:
+  - Manufacturer type MedInria is now obsolete
+  - Manufacturer types AgfaImpax and SyngoVia are obsolete too
+    (use GenericNoWildcardInDates instead)
+  - Obsolete manufacturers are still accepted but might disappear in the future
+  - Added new manufacturer: GenericNoUniversalWildcard to replace all '*' by '' in
+    outgoing C-Find requests
+* New security-related options: "DicomAlwaysAllowStore" and "DicomCheckModalityHost"
+
+REST API
+--------
+
+* Argument "Since" in URI "/tools/find" (related to issue 53)
+* Argument "DicomVersion" in URIs "/{...}/{...}/anonymization"
+
+Plugins
+-------
+
+* New function: "OrthancPluginRegisterIncomingHttpRequestFilter2()"
+
+Lua
+---
+
+* Added HTTP headers support for Lua HttpPost/HttpGet/HttpPut/HttpDelete
+
+Orthanc Explorer
+----------------
+
+* Query/retrieve: Added button for "DR" modality
+
+Maintenance
+-----------
+
+* Ability to retrieve raw frames encoded as unsigned 32-bits integers
+* Fix issue 29 (more consistent handling of the "--upgrade" argument)
+* Fix issue 31 (create new modality types for Philips ADW, GE Xeleris, GE AWServer)
+* Fix issue 35 (AET name is not transferred to Orthanc using DCMTK 3.6.0)
+* Fix issue 44 (bad interpretation of photometric interpretation MONOCHROME1)
+* Fix issue 45 (crash when providing a folder to "--config" command-line option)
+* Fix issue 46 (PHI remaining after anonymization)
+* Fix issue 49 (worklists: accentuated characters are removed from C-Find responses)
+* Fix issue 52 (DICOM level security association problems)
+* Fix issue 55 (modification/anonymization of tags that might break the database
+  model now requires the "Force" parameter to be set to "true" in the query)
+* Fix issue 56 (case-insensitive matching over accents)
+* Fix Debian #865606 (orthanc FTBFS with libdcmtk-dev 3.6.1~20170228-2)
+* Fix XSS inside DICOM in Orthanc Explorer (as reported by Victor Pasnkel, Morphus Labs)
+* Upgrade to DCMTK 3.6.2 in static builds (released on 2017-07-17)
+* Upgrade to Boost 1.64.0 in static builds
+* New advanced "Locale" configuration option
+* Removed configuration option "USE_DCMTK_361_PRIVATE_DIC"
+
+
+Version 1.2.0 (2016/12/13)
+==========================
+
+General
+-------
+
+* Handling of private tags/creators in the "Dictionary" configuration option
+* New configuration options: "LoadPrivateDictionary", "DicomScuTimeout" and "DicomScpTimeout"
+* New metadata automatically computed at the instance level: "TransferSyntax" and "SopClassUid"
+
+REST API
+--------
+
+* "/tools/invalidate-tags" to invalidate the JSON summary of all the DICOM files
+  (useful if private tags are registered, or if changing the default encoding)
+* "Permissive" flag for URI "/modalities/{...}/store" to ignore C-STORE transfer errors
+* "Asynchronous" flag for URIs "/modalities/{...}/store" and "/peers/{...}/store"
+  to avoid waiting for the completion of image transfers
+* Possibility to DELETE "dicom-as-json" attachments to reconstruct the JSON summaries
+  (useful if "Dictionary" has changed)
+* "Keep" option for modifications to keep original DICOM identifiers (advanced feature)
+* "/tools/default-encoding" to get or temporarily change the default encoding
+* "/{resource}/{id}/reconstruct" to reconstruct the main DICOM tags, the JSON summary and
+  the metadata of a resource (useful to compute new metadata, or if using "Keep" above)
+
+Plugins
+-------
+
+* New function: "OrthancPluginRegisterPrivateDictionaryTag()" to register private tags
+* More control over client cache in the ServeFolders plugin
+* New C++ help wrappers in "Plugins/Samples/Common/" to read DICOM datasets from REST API
+* New data structure: "OrthancPluginFindMatcher" to match DICOM against C-FIND queries
+
+Maintenance
+-----------
+
+* Fix handling of encodings in C-FIND requests (including for worklists)
+* Use of DCMTK 3.6.1 dictionary of private tags in standalone builds
+* Avoid hard crash if not enough memory (handling of std::bad_alloc)
+* Improved robustness of Orthanc Explorer wrt. query/retrieve (maybe fix issue 24)
+* Fix serious performance issue with C-FIND
+* Fix extraction of the symbolic name of the private tags
+* Performance warning if runtime debug assertions are turned on
+* Improved robustness against files with no PatientID
+* Upgrade to curl 7.50.3 for static and Windows builds
+* Content-Type for JSON documents is now "application/json; charset=utf-8"
+* Ignore "Group Length" tags in C-FIND queries
+* Fix handling of worklist SCP with ReferencedStudySequence and ReferencedPatientSequence
+* Fix handling of Move Originator AET and ID in C-MOVE SCP
+* Fix vulnerability ZSL-2016-5379 "Unquoted Service Path Privilege Escalation" in the
+  Windows service
+* Fix vulnerability ZSL-2016-5380 "Remote Memory Corruption Vulnerability" in DCMTK 3.6.0
+
+
+Version 1.1.0 (2016/06/27)
+==========================
+
+General
+-------
+
+* HTTPS client certificates can be associated with Orthanc peers to enhance security over Internet
+* Possibility to use PKCS#11 authentication for hardware security modules with Orthanc peers
+* New command-line option "--logfile" to output the Orthanc log to the given file
+* Support of SIGHUP signal (restart Orthanc only if the configuration files have changed)
+
+REST API
+--------
+
+* New URI: "/instances/.../frames/.../raw" to access the raw frames (bypass image decoding)
+* New URI "/modalities/.../move" to issue C-MOVE SCU requests
+* "MoveOriginatorID" can be specified for "/modalities/.../store"
+
+Dicom protocol
+--------------
+
+* Support of optional tags for counting resources in C-FIND:
+  0008-0061, 0008-0062, 0020-1200, 0020-1202, 0020-1204, 0020-1206, 0020-1208, 0020-1209
+* Support of Move Originator Message ID (0000,1031) in C-STORE responses driven by C-MOVE
+
+Plugins
+-------
+
+* Speedup in plugins by removing unnecessary locks
+* New callback to filter incoming HTTP requests: OrthancPluginRegisterIncomingHttpRequestFilter()
+* New callback to handle non-worklists C-FIND requests: OrthancPluginRegisterFindCallback()
+* New callback to handle C-MOVE requests: OrthancPluginRegisterMoveCallback()
+* New function: "OrthancPluginHttpClient()" to do HTTP requests with full control
+* New function: "OrthancPluginGenerateUuid()" to generate a UUID
+* More than one custom image decoder can be installed (e.g. to handle different transfer syntaxes)
+
+Lua
+---
+
+* Possibility to dynamically fix outgoing C-FIND requests using "OutgoingFindRequestFilter()"
+* Access to the HTTP headers in the "IncomingHttpRequestFilter()" callback
+
+Image decoding
+--------------
+
+* Huge speedup if decoding the family of JPEG transfer syntaxes
+* Refactoring leading to speedups with custom image decoders (including Web viewer plugin)
+* Support decoding of RLE Lossless transfer syntax
+* Support of signed 16bpp images in ParsedDicomFile
+
+Maintenance
+-----------
+
+* New logo of Orthanc
+* Fix issue 11 (is_regular_file() fails for FILE_ATTRIBUTE_REPARSE_POINT)
+* Fix issue 16 ("Limit" parameter error in REST API /tools/find method)
+* Fix of Debian bug #818512 ("FTBFS: Please install libdcmtk*-dev")
+* Fix of Debian bug #823139 ("orthanc: Please provide RecoverCompressedFile.cpp")
+* Replaced "localhost" by "127.0.0.1", as it might impact performance on Windows
+* Compatibility with CMake >= 3.5.0
+* Possibility to use forthcoming DCMTK 3.6.1 in static builds (instead of 3.6.0)
+* Upgrade to Boost 1.60.0 for static builds
+* Use of HTTP status 403 Forbidden (instead of 401) if access to a REST resource is disallowed
+* Option "HttpsVerifyPeers" can be used to connect against self-signed HTTPS certificates
+* New configuration option "AllowFindSopClassesInStudy"
+* Macro "__linux" (now obsolete) replaced by macro "__linux__" (maybe solves Debian bug #821011)
+* Modification of instances can now replace PixelData (resp. EncapsulatedDocument) with 
+  provided a PNG/JPEG image (resp. PDF file) if it is encoded using Data URI Scheme
+* Dropped support of Google Log
+
+
+Version 1.0.0 (2015/12/15)
+==========================
+
+* Lua: "IncomingFindRequestFilter()" to apply filters to incoming C-FIND requests
+* New function in plugin SDK: "OrthancPluginSendMultipartItem2()"
+* Fix of DICOMDIR generation with DCMTK 3.6.1, support of encodings
+* Fix range search if the lower or upper limit is absent
+* Fix modality worklists lookups if tags with UN (unknown) VR are present
+* Warn about badly formatted modality/peer definitions in configuration file at startup
+
+
+Version 0.9.6 (2015/12/08)
+==========================
+
+* Promiscuous mode (accept unknown SOP class UID) is now turned off by default
+* Fix serialization of DICOM buffers that might contain garbage trailing
+* Fix modality worklists server if some fields are null
+* More tolerant "/series/.../ordered-slices" with broken series
+* Improved logging information if upgrade fails
+* Fix formatting of multipart HTTP answers (bis)
+
+
+Version 0.9.5 (2015/12/02)
+==========================
+
+Major
+-----
+
+* Experimental support of DICOM C-FIND SCP for modality worklists through plugins
+* Support of DICOM C-FIND SCU for modality worklists ("/modalities/{dicom}/find-worklist")
+
+REST API
+--------
+
+* New URIs:
+  - "/series/.../ordered-slices" to order the slices of a 2D+t or 3D series
+  - "/tools/shutdown" to stop Orthanc from the REST API
+  - ".../compress", ".../uncompress" and ".../is-compressed" for attachments
+  - "/tools/create-archive" to create ZIP from a set of resources
+  - "/tools/create-media" to create ZIP+DICOMDIR from a set of resources
+  - "/instances/.../header" to get the meta information (header) of the DICOM instance
+* "/tools/create-dicom":
+  - Support of binary tags encoded using data URI scheme
+  - Support of hierarchical structures (creation of sequences)
+  - Create tags with unknown VR
+* "/modify" can insert/modify sequences
+* ".../preview" and ".../image-uint8" can return JPEG images if the HTTP Accept Header asks so
+* "Origin" metadata for the instances
+
+Minor
+-----
+
+* New configuration options:
+  - "UnknownSopClassAccepted" to disable promiscuous mode (accept unknown SOP class UID)
+  - New configuration option: "Dictionary" to declare custom DICOM tags
+* Add ".dcm" suffix to files in ZIP archives (cf. URI ".../archive")
+* MIME content type can be associated to custom attachments (cf. "UserContentType")
+
+Plugins
+-------
+
+* New functions:
+  - "OrthancPluginRegisterDecodeImageCallback()" to replace the built-in image decoder
+  - "OrthancPluginDicomInstanceToJson()" to convert DICOM to JSON
+  - "OrthancPluginDicomBufferToJson()" to convert DICOM to JSON
+  - "OrthancPluginRegisterErrorCode()" to declare custom error codes
+  - "OrthancPluginRegisterDictionaryTag()" to declare custom DICOM tags
+  - "OrthancPluginLookupDictionary()" to get information about some DICOM tag
+  - "OrthancPluginRestApiGet2()" to provide HTTP headers when calling Orthanc API
+  - "OrthancPluginGetInstanceOrigin()" to know through which mechanism an instance was received
+  - "OrthancPluginCreateImage()" and "OrthancPluginCreateImageAccessor()" to create images
+  - "OrthancPluginDecodeDicomImage()" to decode DICOM images
+  - "OrthancPluginComputeMd5()" and "OrthancPluginComputeSha1()" to compute MD5/SHA-1 hash
+* New events in change callbacks:
+  - "OrthancStarted"
+  - "OrthancStopped"
+  - "UpdatedAttachment" 
+  - "UpdatedMetadata"
+* "/system" URI gives information about the plugins used for storage area and DB back-end
+* Plugin callbacks must now return explicit "OrthancPluginErrorCode" (instead of integers)
 
 Lua
 ---
@@ -25,15 +496,17 @@
 Maintenance
 -----------
 
-* Full refactoring of the searching features
-* C-Move SCP for studies using AccessionNumber tag
-* Fix issue 4 (C-Store Association not renegotiated on Specific-to-specific transfer syntax change)
-* "/system" URI gives information about the plugins used for storage area and DB back-end
-* Plugin callbacks should now return explicit "OrthancPluginErrorCode" instead of integers
-* "/tools/create-dicom" can create tags with unknown VR
+* Full indexation of the patient/study tags to speed up searches and C-FIND
+* Many refactorings, notably of the searching features and of the image decoding
+* C-MOVE SCP for studies using AccessionNumber tag
+* Fix issue 4 (C-STORE Association not renegotiated on Specific-to-specific transfer syntax change)
+* Fix formatting of multipart HTTP answers
 * "--logdir" flag creates a single log file instead of 3 separate files for errors/warnings/infos
 * "--errors" flag lists the error codes that could be returned by Orthanc
 * Under Windows, the exit status of Orthanc corresponds to the encountered error code
+* New "AgfaImpax", "EFilm2" and "Vitrea" modality manufacturers
+* C-FIND SCP will return tags with sequence value representation
+* Upgrade to Boost 1.59.0 for static builds
 
 
 Version 0.9.4 (2015/09/16)
@@ -77,7 +550,7 @@
 * If error while calling the REST API, the answer body contains description of the error
   (this feature can be disabled with the "HttpDescribeErrors" option)
 * Upgrade to curl 7.44.0 for static and Windows builds
-* Upgrade to libcurl 1.0.2d for static and Windows builds
+* Upgrade to openssl 1.0.2d for static and Windows builds
 * Depends on libjpeg 9a
 * Bypass zlib uncompression if "StorageCompression" is enabled and HTTP client supports deflate
 
@@ -108,7 +581,7 @@
 -------
 
 * The configuration can be splitted into several files stored inside the same folder
-* Custom setting of the local AET during C-Store SCU (both in Lua and in the REST API)
+* Custom setting of the local AET during C-STORE SCU (both in Lua and in the REST API)
 * Many code refactorings
 
 Lua
@@ -127,9 +600,9 @@
 Fixes
 -----
 
-* Fix compatibility issues for C-Find SCU to Siemens Syngo.Via modalities SCP
+* Fix compatibility issues for C-FIND SCU to Siemens Syngo.Via modalities SCP
 * Fix issue 15 (Lua scripts making HTTP requests)
-* Fix issue 35 (Characters in PatientID string are not protected for C-Find)
+* Fix issue 35 (Characters in PatientID string are not protected for C-FIND)
 * Fix issue 37 (Hyphens trigger range query even if datatype does not support ranges)
 
 
@@ -140,7 +613,7 @@
 -----
 
 * DICOM Query/Retrieve available from Orthanc Explorer
-* C-Move SCU and C-Find SCU are accessible through the REST API
+* C-MOVE SCU and C-FIND SCU are accessible through the REST API
 * "?expand" flag for URIs "/patients", "/studies" and "/series"
 * "/tools/find" URI to search for DICOM resources from REST
 * Support of FreeBSD
@@ -150,15 +623,15 @@
 -----
 
 * Speed-up in Orthanc Explorer for large amount of images
-* Speed-up of the C-Find SCP server of Orthanc
+* Speed-up of the C-FIND SCP server of Orthanc
 * Allow replacing PatientID/StudyInstanceUID/SeriesInstanceUID from Lua scripts
-* Option "CaseSensitivePN" to enable case-insensitive C-Find SCP
+* Option "CaseSensitivePN" to enable case-insensitive C-FIND SCP
 
 Fixes
 -----
 
 * Prevent freeze on C-FIND if no DICOM tag is to be returned
-* Fix slow C-Store SCP on recent versions of Linux, if
+* Fix slow C-STORE SCP on recent versions of GNU/Linux, if
   USE_SYSTEM_DCMTK is set to OFF (http://forum.dcmtk.org/viewtopic.php?f=1&t=4009)
 * Fix issue 30 (QR response missing "Query/Retrieve Level" (008,0052))
 * Fix issue 32 (Cyrillic symbols): Introduction of the "Windows1251" encoding
@@ -229,7 +702,7 @@
 
 * "/instances-tags" to get the tags of all the child instances of a
   patient/study/series with a single REST call (bulk tags retrieval)
-* Configuration/Lua to select the accepted C-Store SCP transfer syntaxes
+* Configuration/Lua to select the accepted C-STORE SCP transfer syntaxes
 * Fix reporting of errors in Orthanc Explorer when sending images to peers/modalities
 * Installation of plugin SDK in CMake
 
@@ -340,7 +813,7 @@
 Version 0.7.5 (2014/05/08)
 ==========================
 
-* Dynamic negotiation of SOP classes for C-Store SCU
+* Dynamic negotiation of SOP classes for C-STORE SCU
 * Creation of DICOM instances using the REST API
 * Embedding of images within DICOM instances
 * Adding/removal/modification of remote modalities/peers through REST
@@ -379,16 +852,16 @@
 * Possibility to disable the HTTP server or the DICOM server
 * Automatic computation of MD5 hashes for the stored DICOM files
 * Maintenance tool to recover DICOM files compressed by Orthanc
-* The newline characters in the configuration file are fixed for Linux
-* Capture of the SIGTERM signal in Linux
+* The newline characters in the configuration file are fixed for GNU/Linux
+* Capture of the SIGTERM signal in GNU/Linux
 
 
 Version 0.7.2 (2013/11/08)
 ==========================
 
 * Support of Query/Retrieve from medInria
-* Accept more transfer syntaxes for C-Store SCP and SCU (notably JPEG)
-* Create the meta-header when receiving files through C-Store SCP
+* Accept more transfer syntaxes for C-STORE SCP and SCU (notably JPEG)
+* Create the meta-header when receiving files through C-STORE SCP
 * Fixes and improvements thanks to the static analyzer cppcheck
 
 
@@ -431,7 +904,7 @@
 ==========================
 
 * Detection of stable patients/studies/series
-* C-Find SCU at the instance level
+* C-FIND SCU at the instance level
 * Link from modified to original resource in Orthanc Explorer
 * Fix of issue #8
 * Anonymization of the medical alerts tag (0010,2000)
@@ -541,7 +1014,7 @@
 * The patient/study/series/instances are now indexed by SHA-1 digests
   of their DICOM Instance IDs (and not by UUIDs anymore): The same
   DICOM objects are thus always identified by the same Orthanc IDs
-* Log of exported instances through DICOM C-Store SCU ("/exported" URI)
+* Log of exported instances through DICOM C-STORE SCU ("/exported" URI)
 * Full refactoring of the DB schema and of the REST API
 * Introduction of generic classes for REST APIs (in Core/RestApi)
 
@@ -572,7 +1045,7 @@
 Version 0.2.2 (2012/10/04)
 ==========================
 
-* Switch to Google Logging
+* Switch to Google Log
 * Fixes to Debian packaging
 
 
--- a/OrthancExplorer/explorer.html	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancExplorer/explorer.html	Fri Oct 12 15:18:10 2018 +0200
@@ -34,25 +34,139 @@
     <script src="../plugins/explorer.js"></script>
   </head>
   <body>
-    <div data-role="page" id="find-patients" >
+    <div data-role="page" id="lookup" >
       <div data-role="header" >
-	<h1><span class="orthanc-name"></span>Find a patient</h1>
-        <a href="#plugins" data-icon="grid" class="ui-btn-left" data-direction="reverse">Plugins</a>
+	<h1><span class="orthanc-name"></span>Lookup studies</h1>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
         <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
           <a href="#upload" data-icon="gear" data-role="button">Upload</a>
           <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
         </div>
       </div>
       <div data-role="content">
+        <form data-ajax="false" id="lookup-form">
+          <div data-role="fieldcontain">
+	    <label for="lookup-patient-id">Patient ID:</label>
+	    <input type="text" name="lookup-patient-id" id="lookup-patient-id" value=""  />
+	  </div>
+
+          <div data-role="fieldcontain">
+	    <label for="lookup-patient-name">Patient Name:</label>
+	    <input type="text" name="lookup-patient-name" id="lookup-patient-name" value=""  />
+	  </div>
+
+          <div data-role="fieldcontain">
+	    <label for="lookup-accession-number">Accession Number:</label>
+	    <input type="text" name="lookup-accession-number" id="lookup-accession-number" value=""  />
+	  </div>
+
+          <div data-role="fieldcontain">
+	    <label for="lookup-study-description">Study Description:</label>
+	    <input type="text" name="lookup-study-description" id="lookup-study-description" value=""  />
+	  </div>
+
+          <div data-role="fieldcontain">
+	    <label for="lookup-study-date">Study Date:</label>
+            <select name="lookup-study-date" id="lookup-study-date">
+            </select>
+	  </div>
+
+          <fieldset class="ui-grid-b">
+	    <div class="ui-block-a">
+              <a href="#find-patients" data-role="button" data-theme="b" data-direction="reverse">All patients</a>
+            </div>
+	    <div class="ui-block-b">
+              <a href="#find-studies" data-role="button" data-theme="b" data-direction="reverse">All studies</a>
+            </div>
+	    <div class="ui-block-c">
+              <button id="lookup-submit" type="submit" data-theme="e">Do lookup</button>
+            </div>
+	  </fieldset>
+          <div>&nbsp;</div>
+        </form>
+        <div id="lookup-result">
+          <div id="lookup-alert">
+            <div class="ui-bar ui-bar-e">
+              <h3>Warning:</h3> Your lookup led to many results!
+              Showing only <span id="lookup-count">?</span> studies to
+              avoid performance issue. Please make your query more
+              specific, then relaunch the lookup.
+            </div>
+            <div>&nbsp;</div>
+          </div>
+          <ul data-role="listview" data-inset="true" data-filter="true">
+          </ul>
+        </div>
+      </div>
+    </div>
+
+    <div data-role="page" id="find-patients" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>All patients</h1>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#upload" data-icon="gear" data-role="button">Upload</a>
+          <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
+        </div>
+      </div>
+      <div data-role="content">
+        <div id="alert-patients">
+          <div class="ui-bar ui-bar-e">
+            <h3>Warning:</h3> This is a large Orthanc server. Showing
+            only <span id="count-patients">?</span> patients to avoid
+            performance issue. Make sure to use lookup if targeting
+            specific patients!
+          </div>
+          <div>&nbsp;</div>
+        </div>
         <ul id="all-patients" data-role="listview" data-inset="true" data-filter="true">
         </ul>
       </div>
     </div>
 
-    <div data-role="page" id="upload" >
+    <div data-role="page" id="find-studies" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>All studies</h1>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#upload" data-icon="gear" data-role="button">Upload</a>
+          <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
+        </div>
+      </div>
+      <div data-role="content">
+        <div id="alert-studies">
+          <div class="ui-bar ui-bar-e">
+            <h3>Warning:</h3> This is a large Orthanc server. Showing
+            only <span id="count-studies">?</span> studies to avoid
+            performance issue. Make sure to use lookup if targeting
+            specific studies!
+          </div>
+          <div>&nbsp;</div>
+        </div>
+        <ul id="all-studies" data-role="listview" data-inset="true" data-filter="true">
+        </ul>
+      </div>
+    </div>
+
+    <div data-role="page" id="upload">
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Upload DICOM files</h1>
-        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
       </div>
       <div data-role="content">
         <div style="display:none">
@@ -78,10 +192,14 @@
     <div data-role="page" id="patient" >
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Patient</h1>
-        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
         <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
           <a href="#upload" data-icon="gear" data-role="button">Upload</a>
           <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
         </div>
       </div>
       <div data-role="content">
@@ -135,10 +253,14 @@
           <a href="#" class="patient-link">Patient</a> &raquo; 
           Study
         </h1>
-        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
         <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
           <a href="#upload" data-icon="gear" data-role="button">Upload</a>
           <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
         </div>
       </div>
       <div data-role="content">
@@ -186,11 +308,14 @@
           <a href="#" class="study-link">Study</a> &raquo; 
           Series
         </h1>
-
-        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
         <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
           <a href="#upload" data-icon="gear" data-role="button">Upload</a>
           <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
         </div>
       </div>
       <div data-role="content">
@@ -240,10 +365,14 @@
           <a href="#" class="series-link">Series</a> &raquo; 
           Instance
         </h1>
-        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
         <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
           <a href="#upload" data-icon="gear" data-role="button">Upload</a>
           <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
         </div>
       </div>
       <div data-role="content">
@@ -292,7 +421,10 @@
     <div data-role="page" id="plugins" >
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Plugins</h1>
-        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
       </div>
       <div data-role="content">
         <ul id="all-plugins" data-role="listview" data-inset="true" data-filter="true">
@@ -302,8 +434,11 @@
 
     <div data-role="page" id="query-retrieve" >
       <div data-role="header" >
-	<h1><span class="orthanc-name"></span>DICOM Query/Retrieve (1/3)</h1>
-        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+	<h1><span class="orthanc-name"></span>DICOM Query/Retrieve (1/4)</h1>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
       </div>
       <div data-role="content">
         <form data-ajax="false">
@@ -349,6 +484,8 @@
 	        <input type="checkbox" name="PT" id="qr-pt" class="custom" /> <label for="qr-pt">PT</label>
 	        <input type="checkbox" name="US" id="qr-us" class="custom" /> <label for="qr-us">US</label>
 	        <input type="checkbox" name="XA" id="qr-xa" class="custom" /> <label for="qr-xa">XA</label>
+	        <input type="checkbox" name="DR" id="qr-dr" class="custom" /> <label for="qr-dr">DR</label>
+	        <input type="checkbox" name="DX" id="qr-dx" class="custom" /> <label for="qr-dx">DX</label>
 	      </fieldset>
             </div>
           </div>
@@ -368,8 +505,11 @@
 
     <div data-role="page" id="query-retrieve-2" >
       <div data-role="header" >
-	<h1><span class="orthanc-name"></span>DICOM Query/Retrieve (2/3)</h1>
-        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+	<h1><span class="orthanc-name"></span>DICOM Query/Retrieve (2/4)</h1>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
         <a href="#query-retrieve" data-icon="search" class="ui-btn-right" data-direction="reverse">Query/Retrieve</a>
       </div>
       <div data-role="content">
@@ -381,17 +521,91 @@
 
     <div data-role="page" id="query-retrieve-3" >
       <div data-role="header" >
-	<h1><span class="orthanc-name"></span>DICOM Query/Retrieve (3/3)</h1>
-        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+	<h1><span class="orthanc-name"></span>DICOM Query/Retrieve (3/4)</h1>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
         <a href="#query-retrieve" data-icon="search" class="ui-btn-right" data-direction="reverse">Query/Retrieve</a>
       </div>
       <div data-role="content">
-        <ul data-role="listview" data-inset="true" data-filter="true" data-split-icon="arrow-d" data-split-theme="b">
+        <ul data-role="listview" data-inset="true" data-filter="true">
         </ul>
       </div>
     </div>
 
 
+    <div data-role="page" id="query-retrieve-4" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>DICOM Query/Retrieve (4/4)</h1>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
+        <a href="#query-retrieve" data-icon="search" class="ui-btn-right" data-direction="reverse">Query/Retrieve</a>
+      </div>
+
+      <div data-role="content">
+        <form data-ajax="false" id="retrieve-form">
+          <div data-role="fieldcontain">
+	    <label for="retrieve-target">Target AET:</label>
+            <input type="text" name="retrieve-target" id="retrieve-target"></input>
+	  </div>
+
+          <fieldset class="ui-grid-b">
+	    <div class="ui-block-a"></div>
+	    <div class="ui-block-b">
+              <button id="retrieve-submit" type="submit" data-theme="b">Retrieve</button>
+            </div>
+	    <div class="ui-block-c"></div>
+	  </fieldset>
+        </form>
+      </div>
+    </div>
+
+    
+    <div data-role="page" id="jobs" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>Jobs</h1>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
+      </div>
+      <div data-role="content">
+        <ul id="all-jobs" data-role="listview" data-inset="true" data-filter="true">
+        </ul>
+      </div>
+    </div>
+
+    <div data-role="page" id="job" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>Job</h1>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#lookup" data-icon="arrow-r" data-role="button" data-direction="reverse">Lookup</a>
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+        </div>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
+        </div>
+      </div>
+      <div data-role="content">
+        <ul data-role="listview" data-inset="true" data-filter="true" id="job-info">
+        </ul>
+
+        <fieldset class="ui-grid-b">
+          <div class="ui-block-a"></div>
+	  <div class="ui-block-b">
+            <button id="job-cancel" data-theme="b">Cancel job</button>         
+            <button id="job-resubmit" data-theme="b">Resubmit job</button>         
+            <button id="job-pause" data-theme="b">Pause job</button>         
+            <button id="job-resume" data-theme="b">Resume job</button>         
+          </div>
+          <div class="ui-block-c"></div>
+	</fieldset>
+      </div>
+    </div>
+
     <div id="peer-store" style="display:none;" class="ui-body-c">
       <p align="center"><b>Sending to Orthanc peer...</b></p>
       <p><img src="libs/images/ajax-loader.gif" alt="" /></p>
--- a/OrthancExplorer/explorer.js	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancExplorer/explorer.js	Fri Oct 12 15:18:10 2018 +0200
@@ -13,21 +13,40 @@
 //$.mobile.defaultPageTransition = 'slide';
 
 
+var LIMIT_RESOURCES = 100;
+
 var currentPage = '';
 var currentUuid = '';
 
 
-// http://stackoverflow.com/a/4673436
-String.prototype.format = function() {
-  var args = arguments;
-  return this.replace(/{(\d+)}/g, function(match, number) { 
-    /*return typeof args[number] != 'undefined'
-      ? args[number]
-      : match;*/
+function DeepCopy(obj)
+{
+  return jQuery.extend(true, {}, obj);
+}
+
 
-    return args[number];
-  });
-};
+function ChangePage(page, options)
+{
+  var first = true;
+  if (options) {
+    for (var key in options) {
+      var value = options[key];
+      if (first) {
+        page += '?';
+        first = false;
+      } else {
+        page += '&';
+      }
+      
+      page += key + '=' + value;
+    }
+  }
+
+  window.location.replace('explorer.html#' + page);
+  /*$.mobile.changePage('#' + page, {
+    changeHash: true
+  });*/
+}
 
 
 function Refresh()
@@ -179,29 +198,34 @@
 }
 
 
-function CompleteFormatting(s, link, isReverse)
+function CompleteFormatting(node, link, isReverse, count)
 {
+  if (count != null)
+  {
+    node = node.add($('<span>')
+                    .addClass('ui-li-count')
+                    .text(count));
+  }
+  
   if (link != null)
   {
-    s = 'href="' + link + '">' + s + '</a>';
-    
+    node = $('<a>').attr('href', link).append(node);
+
     if (isReverse)
-      s = 'data-direction="reverse" '+ s;
-
-    s = '<a ' + s;
+      node.attr('data-direction', 'reverse')
   }
 
+  node = $('<li>').append(node);
+
   if (isReverse)
-    return '<li data-icon="back">' + s + '</li>';
-  else
-    return '<li>' + s + '</li>';
+    node.attr('data-icon', 'back');
+
+  return node;
 }
 
 
-function FormatMainDicomTags(tags, tagsToIgnore)
+function FormatMainDicomTags(target, tags, tagsToIgnore)
 {
-  var s = '';
-
   for (var i in tags)
   {
     if (tagsToIgnore.indexOf(i) == -1)
@@ -220,47 +244,52 @@
         v = SplitLongUid(v);
       }
       
-
-      s += ('<p>{0}: <strong>{1}</strong></p>').format(i, v);
+      target.append($('<p>')
+                    .text(i + ': ')
+                    .append($('<strong>').text(v)));
     }
   }
-
-  return s;
 }
 
 
 function FormatPatient(patient, link, isReverse)
 {
-  var s = ('<h3>{0}</h3>{1}' + 
-           '<span class="ui-li-count">{2}</span>'
-          ).format
-  (patient.MainDicomTags.PatientName,
-   FormatMainDicomTags(patient.MainDicomTags, [ 
-     "PatientName"
-     /*"OtherPatientIDs" */
-   ]),
-   patient.Studies.length
-  );
+  var node = $('<div>').append($('<h3>').text(patient.MainDicomTags.PatientName));
 
-  return CompleteFormatting(s, link, isReverse);
+  FormatMainDicomTags(node, patient.MainDicomTags, [ 
+    "PatientName"
+    // "OtherPatientIDs"
+  ]);
+    
+  return CompleteFormatting(node, link, isReverse, patient.Studies.length);
 }
 
 
 
-function FormatStudy(study, link, isReverse)
+function FormatStudy(study, link, isReverse, includePatient)
 {
-  var s = ('<h3>{0}</h3>{1}' +
-           '<span class="ui-li-count">{2}</span>'
-           ).format
-  (study.MainDicomTags.StudyDescription,
-   FormatMainDicomTags(study.MainDicomTags, [
-     "StudyDescription", 
-     "StudyTime" 
-   ]),
-   study.Series.length
-  );
+  var label;
+
+  if (includePatient) {
+    label = study.Label;
+  } else {
+    label = study.MainDicomTags.StudyDescription;
+  }
+
+  var node = $('<div>').append($('<h3>').text(label));
 
-  return CompleteFormatting(s, link, isReverse);
+  if (includePatient) {
+    FormatMainDicomTags(node, study.PatientMainDicomTags, [ 
+      'PatientName'
+    ]);
+  }
+    
+  FormatMainDicomTags(node, study.MainDicomTags, [ 
+     'StudyDescription', 
+     'StudyTime' 
+  ]);
+
+  return CompleteFormatting(node, link, isReverse, study.Series.length);
 }
 
 
@@ -277,41 +306,39 @@
   {
     c = series.Instances.length + '/' + series.ExpectedNumberOfInstances;
   }
+  
+  var node = $('<div>')
+      .append($('<h3>').text(series.MainDicomTags.SeriesDescription))
+      .append($('<p>').append($('<em>')
+                           .text('Status: ')
+                           .append($('<strong>').text(series.Status))));
 
-  var s = ('<h3>{0}</h3>' +
-           '<p><em>Status: <strong>{1}</strong></em></p>{2}' +
-           '<span class="ui-li-count">{3}</span>').format
-  (series.MainDicomTags.SeriesDescription,
-   series.Status,
-   FormatMainDicomTags(series.MainDicomTags, [
+  FormatMainDicomTags(node, series.MainDicomTags, [ 
      "SeriesDescription", 
      "SeriesTime", 
      "Manufacturer",
      "ImagesInAcquisition",
      "SeriesDate",
      "ImageOrientationPatient"
-   ]),
-   c
-  );
-
-  return CompleteFormatting(s, link, isReverse);
+  ]);
+    
+  return CompleteFormatting(node, link, isReverse, c);
 }
 
 
 function FormatInstance(instance, link, isReverse)
 {
-  var s = ('<h3>Instance {0}</h3>{1}').format
-  (instance.IndexInSeries,
-   FormatMainDicomTags(instance.MainDicomTags, [
-     "AcquisitionNumber", 
-     "InstanceNumber", 
-     "InstanceCreationDate", 
-     "InstanceCreationTime",
-     "ImagePositionPatient"
-   ])
-  );
+  var node = $('<div>').append($('<h3>').text('Instance: ' + instance.IndexInSeries));
 
-  return CompleteFormatting(s, link, isReverse);
+  FormatMainDicomTags(node, instance.MainDicomTags, [
+    "AcquisitionNumber", 
+    "InstanceNumber", 
+    "InstanceCreationDate", 
+    "InstanceCreationTime",
+    "ImagePositionPatient"
+  ]);
+    
+  return CompleteFormatting(node, link, isReverse);
 }
 
 
@@ -323,7 +350,11 @@
     cache: false,
     success: function(s) {
       if (s.Name != "") {
-        $('.orthanc-name').html('<a class="ui-link" href="explorer.html">' + s.Name + '</a> &raquo; ');
+        $('.orthanc-name').html($('<a>')
+                                .addClass('ui-link')
+                                .attr('href', 'explorer.html')
+                                .text(s.Name)
+                                .append(' &raquo; '));
       }
     }
   });
@@ -331,19 +362,166 @@
 
 
 
+$('#lookup').live('pagebeforeshow', function() {
+  // NB: "GenerateDicomDate()" is defined in "query-retrieve.js"
+  var target = $('#lookup-study-date');
+  $('option', target).remove();
+  target.append($('<option>').attr('value', '*').text('Any date'));
+  target.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-1)).text('Yesterday'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-7) + '-').text('Last 7 days'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-31) + '-').text('Last 31 days'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-31 * 3) + '-').text('Last 3 months'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-365) + '-').text('Last year'));
+  target.selectmenu('refresh');
+
+  $('#lookup-result').hide();
+});
+
+
+$('#lookup-submit').live('click', function() {
+  $('#lookup-result').hide();
+
+  var lookup = {
+    'Level' : 'Study',
+    'Expand' : true,
+    'Limit' : LIMIT_RESOURCES + 1,
+    'Query' : {
+      'StudyDate' : $('#lookup-study-date').val()
+    }
+  };
+
+  $('#lookup-form input').each(function(index, input) {
+    if (input.value.length != 0) {
+      if (input.id == 'lookup-patient-id') {
+        lookup['Query']['PatientID'] = input.value;
+      } 
+      else if (input.id == 'lookup-patient-name') {
+        lookup['Query']['PatientName'] = input.value;
+      } 
+      else if (input.id == 'lookup-accession-number') {
+        lookup['Query']['AccessionNumber'] = input.value;
+      } 
+      else if (input.id == 'lookup-study-description') {
+        lookup['Query']['StudyDescription'] = input.value;
+      }
+      else {
+        console.error('Unknown lookup field: ' + input.id);
+      }
+    } 
+  });
+
+  $.ajax({
+    url: '../tools/find',
+    type: 'POST', 
+    data: JSON.stringify(lookup),
+    dataType: 'json',
+    async: false,
+    error: function() {
+      alert('Error during lookup');
+    },
+    success: function(studies) {
+      FormatListOfStudies('#lookup-result ul', '#lookup-alert', '#lookup-count', studies);
+      $('#lookup-result').show();
+    }
+  });
+
+  return false;
+});
+
+
 $('#find-patients').live('pagebeforeshow', function() {
-  GetResource('/patients?expand', function(patients) {
-      var target = $('#all-patients');
-      $('li', target).remove();
+  GetResource('/patients?expand&since=0&limit=' + (LIMIT_RESOURCES + 1), function(patients) {
+    var target = $('#all-patients');
+    $('li', target).remove();
     
-      SortOnDicomTag(patients, 'PatientName', false, false);
+    SortOnDicomTag(patients, 'PatientName', false, false);
+
+    var count, showAlert;
+    if (patients.length <= LIMIT_RESOURCES) {
+      count = patients.length;
+      showAlert = false;
+    }
+    else {
+      count = LIMIT_RESOURCES;
+      showAlert = true;
+    }
+
+    for (var i = 0; i < count; i++) {
+      var p = FormatPatient(patients[i], '#patient?uuid=' + patients[i].ID);
+      target.append(p);
+    }
+
+    target.listview('refresh'); 
 
-      for (var i = 0; i < patients.length; i++) {
-        var p = FormatPatient(patients[i], '#patient?uuid=' + patients[i].ID);
-        target.append(p);
+    if (showAlert) {
+      $('#count-patients').text(LIMIT_RESOURCES);
+      $('#alert-patients').show();
+    } else {
+      $('#alert-patients').hide();
+    }
+  });
+});
+
+
+
+function FormatListOfStudies(targetId, alertId, countId, studies)
+{
+  var target = $(targetId);
+  $('li', target).remove();
+
+  for (var i = 0; i < studies.length; i++) {
+    var patient = studies[i].PatientMainDicomTags.PatientName;
+    var study = studies[i].MainDicomTags.StudyDescription;
+
+    var s;
+    if (typeof patient === 'string') {
+      s = patient;
+    }
+
+    if (typeof study === 'string') {
+      if (s.length > 0) {
+        s += ' - ';
       }
 
-      target.listview('refresh');
+      s += study;
+    }
+
+    studies[i]['Label'] = s;
+  }
+
+  Sort(studies, function(a) { return a.Label }, false, false);
+
+
+  var count, showAlert;
+  if (studies.length <= LIMIT_RESOURCES) {
+    count = studies.length;
+    showAlert = false;
+  }
+  else {
+    count = LIMIT_RESOURCES;
+    showAlert = true;
+  }
+
+  for (var i = 0; i < count; i++) {
+    var p = FormatStudy(studies[i], '#study?uuid=' + studies[i].ID, false, true);
+    target.append(p);
+  }
+
+  target.listview('refresh');
+
+  if (showAlert) {
+    $(countId).text(LIMIT_RESOURCES);
+    $(alertId).show();
+  } else {
+    $(alertId).hide();
+  }
+}
+
+
+$('#find-studies').live('pagebeforeshow', function() {
+  GetResource('/studies?expand&since=0&limit=' + (LIMIT_RESOURCES + 1), function(studies) {
+    FormatListOfStudies('#all-studies', '#alert-studies', '#count-studies', studies);
   });
 });
 
@@ -369,8 +547,10 @@
 function RefreshPatient()
 {
   if ($.mobile.pageData) {
-    GetResource('/patients/' + $.mobile.pageData.uuid, function(patient) {
-      GetResource('/patients/' + $.mobile.pageData.uuid + '/studies', function(studies) {
+    var pageData = DeepCopy($.mobile.pageData);
+
+    GetResource('/patients/' + pageData.uuid, function(patient) {
+      GetResource('/patients/' + pageData.uuid + '/studies', function(studies) {
         SortOnDicomTag(studies, 'StudyDate', false, true);
 
         $('#patient-info li').remove();
@@ -385,8 +565,9 @@
         for (var i = 0; i < studies.length; i++) {
           if (i == 0 || studies[i].MainDicomTags.StudyDate != studies[i - 1].MainDicomTags.StudyDate)
           {
-            target.append('<li data-role="list-divider">{0}</li>'.format
-                          (FormatDicomDate(studies[i].MainDicomTags.StudyDate)));
+            target.append($('<li>')
+                          .attr('data-role', 'list-divider')
+                          .text(FormatDicomDate(studies[i].MainDicomTags.StudyDate)));
           }
 
           target.append(FormatStudy(studies[i], '#study?uuid=' + studies[i].ID));
@@ -399,7 +580,7 @@
 
         // Check whether this patient is protected
         $.ajax({
-          url: '../patients/' + $.mobile.pageData.uuid + '/protected',
+          url: '../patients/' + pageData.uuid + '/protected',
           type: 'GET',
           dataType: 'text',
           async: false,
@@ -411,7 +592,7 @@
         });
 
         currentPage = 'patient';
-        currentUuid = $.mobile.pageData.uuid;
+        currentUuid = pageData.uuid;
       });
     });
   }
@@ -421,9 +602,11 @@
 function RefreshStudy()
 {
   if ($.mobile.pageData) {
-    GetResource('/studies/' + $.mobile.pageData.uuid, function(study) {
+    var pageData = DeepCopy($.mobile.pageData);
+
+    GetResource('/studies/' + pageData.uuid, function(study) {
       GetResource('/patients/' + study.ParentPatient, function(patient) {
-        GetResource('/studies/' + $.mobile.pageData.uuid + '/series', function(series) {
+        GetResource('/studies/' + pageData.uuid + '/series', function(series) {
           SortOnDicomTag(series, 'SeriesDate', false, true);
 
           $('#study .patient-link').attr('href', '#patient?uuid=' + patient.ID);
@@ -443,15 +626,17 @@
           for (var i = 0; i < series.length; i++) {
             if (i == 0 || series[i].MainDicomTags.SeriesDate != series[i - 1].MainDicomTags.SeriesDate)
             {
-              target.append('<li data-role="list-divider">{0}</li>'.format
-                            (FormatDicomDate(series[i].MainDicomTags.SeriesDate)));
+              target.append($('<li>')
+                            .attr('data-role', 'list-divider')
+                            .text(FormatDicomDate(series[i].MainDicomTags.SeriesDate)));
             }
+            
             target.append(FormatSeries(series[i], '#series?uuid=' + series[i].ID));
           }
           target.listview('refresh');
 
           currentPage = 'study';
-          currentUuid = $.mobile.pageData.uuid;
+          currentUuid = pageData.uuid;
         });
       });
     });
@@ -462,10 +647,12 @@
 function RefreshSeries() 
 {
   if ($.mobile.pageData) {
-    GetResource('/series/' + $.mobile.pageData.uuid, function(series) {
+    var pageData = DeepCopy($.mobile.pageData);
+
+    GetResource('/series/' + pageData.uuid, function(series) {
       GetResource('/studies/' + series.ParentStudy, function(study) {
         GetResource('/patients/' + study.ParentPatient, function(patient) {
-          GetResource('/series/' + $.mobile.pageData.uuid + '/instances', function(instances) {
+          GetResource('/series/' + pageData.uuid + '/instances', function(instances) {
             Sort(instances, function(x) { return x.IndexInSeries; }, true, false);
 
             $('#series .patient-link').attr('href', '#patient?uuid=' + patient.ID);
@@ -492,7 +679,7 @@
             target.listview('refresh');
 
             currentPage = 'series';
-            currentUuid = $.mobile.pageData.uuid;
+            currentUuid = pageData.uuid;
           });
         });
       });
@@ -501,6 +688,24 @@
 }
 
 
+function EscapeHtml(value)
+{
+  var ENTITY_MAP = {
+    '&': '&amp;',
+    '<': '&lt;',
+    '>': '&gt;',
+    '"': '&quot;',
+    "'": '&#39;',
+    '/': '&#x2F;',
+    '`': '&#x60;',
+    '=': '&#x3D;'
+  };
+
+  return String(value).replace(/[&<>"'`=\/]/g, function (s) {
+    return ENTITY_MAP[s];
+  });
+}
+
 
 function ConvertForTree(dicom)
 {
@@ -508,12 +713,14 @@
 
   for (var i in dicom) {
     if (dicom[i] != null) {
-      var label = i + '<span class="tag-name"> (<i>' + dicom[i]["Name"] + '</i>)</span>: ';
+      var label = (i + '<span class="tag-name"> (<i>' +
+                   EscapeHtml(dicom[i]["Name"]) +
+                   '</i>)</span>: ');
 
       if (dicom[i]["Type"] == 'String')
       {
         result.push({
-          label: label + '<strong>' + dicom[i]["Value"] + '</strong>',
+          label: label + '<strong>' + EscapeHtml(dicom[i]["Value"]) + '</strong>',
           children: []
         });
       }
@@ -556,7 +763,9 @@
 function RefreshInstance()
 {
   if ($.mobile.pageData) {
-    GetResource('/instances/' + $.mobile.pageData.uuid, function(instance) {
+    var pageData = DeepCopy($.mobile.pageData);
+
+    GetResource('/instances/' + pageData.uuid, function(instance) {
       GetResource('/series/' + instance.ParentSeries, function(series) {
         GetResource('/studies/' + series.ParentStudy, function(study) {
           GetResource('/patients/' + study.ParentPatient, function(patient) {
@@ -585,7 +794,7 @@
             SetupAnonymizedOrModifiedFrom('#instance-modified-from', instance, 'instance', 'ModifiedFrom');
 
             currentPage = 'instance';
-            currentUuid = $.mobile.pageData.uuid;
+            currentUuid = pageData.uuid;
           });
         });
       });
@@ -704,7 +913,9 @@
 
 $('#instance-preview').live('click', function(e) {
   if ($.mobile.pageData) {
-    var pdf = '../instances/' + $.mobile.pageData.uuid + '/pdf';
+    var pageData = DeepCopy($.mobile.pageData);
+
+    var pdf = '../instances/' + pageData.uuid + '/pdf';
     $.ajax({
       url: pdf,
       cache: false,
@@ -712,11 +923,11 @@
         window.location.assign(pdf);
       },
       error: function() {
-        GetResource('/instances/' + $.mobile.pageData.uuid + '/frames', function(frames) {
+        GetResource('/instances/' + pageData.uuid + '/frames', function(frames) {
           if (frames.length == 1)
           {
             // Viewing a single-frame image
-            jQuery.slimbox('../instances/' + $.mobile.pageData.uuid + '/preview', '', {
+            jQuery.slimbox('../instances/' + pageData.uuid + '/preview', '', {
               overlayFadeDuration : 1,
               resizeDuration : 1,
               imageFadeDuration : 1
@@ -728,7 +939,7 @@
 
             var images = [];
             for (var i = 0; i < frames.length; i++) {
-              images.push([ '../instances/' + $.mobile.pageData.uuid + '/frames/' + i + '/preview' ]);
+              images.push([ '../instances/' + pageData.uuid + '/frames/' + i + '/preview' ]);
             }
 
             jQuery.slimbox(images, 0, {
@@ -748,14 +959,16 @@
 
 $('#series-preview').live('click', function(e) {
   if ($.mobile.pageData) {
-    GetResource('/series/' + $.mobile.pageData.uuid, function(series) {
-      GetResource('/series/' + $.mobile.pageData.uuid + '/instances', function(instances) {
+    var pageData = DeepCopy($.mobile.pageData);
+
+    GetResource('/series/' + pageData.uuid, function(series) {
+      GetResource('/series/' + pageData.uuid + '/instances', function(instances) {
         Sort(instances, function(x) { return x.IndexInSeries; }, true, false);
 
         var images = [];
         for (var i = 0; i < instances.length; i++) {
           images.push([ '../instances/' + instances[i].ID + '/preview',
-                        '{0}/{1}'.format(i + 1, instances.length) ])
+                        (i + 1).toString() + '/' + instances.length.toString() ])
         }
 
         jQuery.slimbox(images, 0, {
@@ -858,6 +1071,8 @@
 
 $('#instance-store,#series-store,#study-store,#patient-store').live('click', function(e) {
   ChooseDicomModality(function(modality, peer) {
+    var pageData = DeepCopy($.mobile.pageData);
+
     var url;
     var loading;
 
@@ -878,7 +1093,7 @@
         url: url,
         type: 'POST',
         dataType: 'text',
-        data: $.mobile.pageData.uuid,
+        data: pageData.uuid,
         async: true,  // Necessary to block UI
         beforeSend: function() {
           $.blockUI({ message: $(loading) });
@@ -1052,3 +1267,209 @@
     }
   });
 });
+
+
+
+function ParseJobTime(s)
+{
+  var t = (s.substr(0, 4) + '-' +
+           s.substr(4, 2) + '-' +
+           s.substr(6, 5) + ':' +
+           s.substr(11, 2) + ':' +
+           s.substr(13));
+  var utc = new Date(t);
+
+  // Convert from UTC to local time
+  return new Date(utc.getTime() - utc.getTimezoneOffset() * 60000);
+}
+
+
+function AddJobField(target, description, field)
+{
+  if (!(typeof field === 'undefined')) {
+    target.append($('<p>')
+                  .text(description)
+                  .append($('<strong>').text(field)));
+  }
+}
+
+
+function AddJobDateField(target, description, field)
+{
+  if (!(typeof field === 'undefined')) {
+    target.append($('<p>')
+                  .text(description)
+                  .append($('<strong>').text(ParseJobTime(field))));
+  }
+}
+
+
+$('#jobs').live('pagebeforeshow', function() {
+  $.ajax({
+    url: '../jobs?expand',
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(jobs) {
+      var target = $('#all-jobs');
+      $('li', target).remove();
+
+      var running = $('<li>')
+          .attr('data-role', 'list-divider')
+          .text('Currently running');
+
+      var pending = $('<li>')
+          .attr('data-role', 'list-divider')
+          .text('Pending jobs');
+
+      var inactive = $('<li>')
+          .attr('data-role', 'list-divider')
+          .text('Inactive jobs');
+
+      target.append(running);
+      target.append(pending);
+      target.append(inactive);
+
+      jobs.map(function(job) {
+        var li = $('<li>');
+        var item = $('<a>');
+        li.append(item);
+        item.attr('href', '#job?uuid=' + job.ID);
+        item.append($('<h1>').text(job.Type));
+        item.append($('<span>').addClass('ui-li-count').text(job.State));
+        AddJobField(item, 'ID: ', job.ID);
+        AddJobField(item, 'Local AET: ', job.Content.LocalAet);
+        AddJobField(item, 'Remote AET: ', job.Content.RemoteAet);
+        AddJobDateField(item, 'Creation time: ', job.CreationTime);
+        AddJobDateField(item, 'Completion time: ', job.CompletionTime);
+        AddJobDateField(item, 'ETA: ', job.EstimatedTimeOfArrival);
+
+        if (job.State == 'Running' ||
+            job.State == 'Pending' ||
+            job.State == 'Paused') {
+          AddJobField(item, 'Priority: ', job.Priority);
+          AddJobField(item, 'Progress: ', job.Progress);
+        }
+        
+        if (job.State == 'Running') {
+          li.insertAfter(running);
+        } else if (job.State == 'Pending' ||
+                   job.State == 'Paused') {
+          li.insertAfter(pending);
+        } else {
+          li.insertAfter(inactive);
+        }
+      });
+
+      target.listview('refresh');
+    }
+  });
+});
+
+
+$('#job').live('pagebeforeshow', function() {
+  if ($.mobile.pageData) {
+    var pageData = DeepCopy($.mobile.pageData);
+
+    $.ajax({
+      url: '../jobs/' + pageData.uuid,
+      dataType: 'json',
+      async: false,
+      cache: false,
+      success: function(job) {
+        var target = $('#job-info');
+        $('li', target).remove();
+
+        target.append($('<li>')
+                      .attr('data-role', 'list-divider')
+                      .text('General information about the job'));
+
+        var block = $('<li>');
+        for (var i in job) {
+          if (i == 'CreationTime' ||
+              i == 'CompletionTime' ||
+              i == 'EstimatedTimeOfArrival') {
+            AddJobDateField(block, i + ': ', job[i]);
+          } else if (i != 'InternalContent' &&
+                     i != 'Content' &&
+                     i != 'Timestamp') {
+            AddJobField(block, i + ': ', job[i]);
+          }
+        }
+
+        target.append(block);
+        
+        target.append($('<li>')
+                      .attr('data-role', 'list-divider')
+                      .text('Detailed information'));
+
+        var block = $('<li>');
+
+        for (var item in job.Content) {
+          var value = job.Content[item];
+          if (typeof value !== 'string') {
+            value = JSON.stringify(value);
+          }
+          
+          AddJobField(block, item + ': ', value);
+        }
+
+        target.append(block);
+        
+        target.listview('refresh');
+
+        $('#job-cancel').closest('.ui-btn').hide();
+        $('#job-retry').closest('.ui-btn').hide();
+        $('#job-resubmit').closest('.ui-btn').hide();
+        $('#job-pause').closest('.ui-btn').hide();
+        $('#job-resume').closest('.ui-btn').hide();
+
+        if (job.State == 'Running' ||
+            job.State == 'Pending' ||
+            job.State == 'Retry') {
+          $('#job-cancel').closest('.ui-btn').show();
+          $('#job-pause').closest('.ui-btn').show();
+        }
+        else if (job.State == 'Success') {
+        }
+        else if (job.State == 'Failure') {
+          $('#job-resubmit').closest('.ui-btn').show();
+        }
+        else if (job.State == 'Paused') {
+          $('#job-resume').closest('.ui-btn').show();
+        }
+      }
+    });
+  }
+});
+
+
+
+function TriggerJobAction(action)
+{
+  $.ajax({
+    url: '../jobs/' + $.mobile.pageData.uuid + '/' + action,
+    type: 'POST',
+    async: false,
+    cache: false,
+    complete: function(s) {
+      window.location.reload();
+    }
+  });
+}
+
+$('#job-cancel').live('click', function() {
+  TriggerJobAction('cancel');
+});
+
+$('#job-resubmit').live('click', function() {
+  TriggerJobAction('resubmit');
+});
+
+$('#job-pause').live('click', function() {
+  TriggerJobAction('pause');
+});
+
+$('#job-resume').live('click', function() {
+  TriggerJobAction('resume');
+});
--- a/OrthancExplorer/file-upload.js	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancExplorer/file-upload.js	Fri Oct 12 15:18:10 2018 +0200
@@ -48,6 +48,11 @@
 
 
 $('#upload').live('pageshow', function() {
+  alert('WARNING - This page is currently affected by Orthanc issue #21: ' +
+        '"DICOM files might be missing after uploading with Mozilla Firefox." ' +
+        'Do not use this upload feature for clinical uses, or carefully ' +
+        'check that all instances have been properly received by Orthanc. ' +
+        'Please use the command-line "ImportDicomFiles.py" script to circumvent this issue.');
   $('#fileupload').fileupload('enable');
 });
 
--- a/OrthancExplorer/query-retrieve.js	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancExplorer/query-retrieve.js	Fri Oct 12 15:18:10 2018 +0200
@@ -1,294 +1,322 @@
-function JavascriptDateToDicom(date)
-{
-  var s = date.toISOString();
-  return s.substring(0, 4) + s.substring(5, 7) + s.substring(8, 10);
-}
-
-function GenerateDicomDate(days)
-{
-  var today = new Date();
-  var other = new Date(today);
-  other.setDate(today.getDate() + days);
-  return JavascriptDateToDicom(other);
-}
-
-
-$('#query-retrieve').live('pagebeforeshow', function() {
-  $.ajax({
-    url: '../modalities',
-    dataType: 'json',
-    async: false,
-    cache: false,
-    success: function(modalities) {
-      var target = $('#qr-server');
-      $('option', target).remove();
-
-      for (var i = 0; i < modalities.length; i++) {
-        var option = $('<option>').text(modalities[i]);
-        target.append(option);
-      }
-
-      target.selectmenu('refresh');
-    }
-  });
-
-  var target = $('#qr-date');
-  $('option', target).remove();
-  target.append($('<option>').attr('value', '*').text('Any date'));
-  target.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today'));
-  target.append($('<option>').attr('value', GenerateDicomDate(-1)).text('Yesterday'));
-  target.append($('<option>').attr('value', GenerateDicomDate(-7) + '-').text('Last 7 days'));
-  target.append($('<option>').attr('value', GenerateDicomDate(-31) + '-').text('Last 31 days'));
-  target.append($('<option>').attr('value', GenerateDicomDate(-31 * 3) + '-').text('Last 3 months'));
-  target.append($('<option>').attr('value', GenerateDicomDate(-365) + '-').text('Last year'));
-  target.selectmenu('refresh');
-});
-
-
-$('#qr-echo').live('click', function() {
-  var server = $('#qr-server').val();
-  var message = 'Error: The C-Echo has failed!';
-
-  $.ajax({
-    url: '../modalities/' + server + '/echo',
-    type: 'POST', 
-    cache: false,
-    async: false,
-    success: function() {
-      message = 'The C-Echo has succeeded!';
-    }
-  });
-
-  $('<div>').simpledialog2({
-    mode: 'button',
-    headerText: 'Echo result',
-    headerClose: true,
-    buttonPrompt: message,
-    animate: false,
-    buttons : {
-      'OK': { click: function () { } }
-    }
-  });
-
-  return false;
-});
-
-
-$('#qr-submit').live('click', function() {
-  var query = {
-    'Level' : 'Study',
-    'Query' : {
-      'AccessionNumber' : '*',
-      'PatientBirthDate' : '*',
-      'PatientID' : '*',
-      'PatientName' : '*',
-      'PatientSex' : '*',
-      'SpecificCharacterSet' : 'ISO_IR 192',  // UTF-8
-      'StudyDate' : $('#qr-date').val(),
-      'StudyDescription' : '*'
-    }
-  };
-
-  var field = $('#qr-fields input:checked').val();
-  query['Query'][field] = $('#qr-value').val().toUpperCase();
-
-  var modalities = '';
-  $('#qr-modalities input:checked').each(function() {
-    var s = $(this).attr('name');
-    if (modalities == '*')
-      modalities = s;
-    else
-      modalities += '\\' + s;
-  });
-
-  if (modalities.length > 0) {
-    query['Query']['ModalitiesInStudy'] = modalities;
-  }
-
-
-  var server = $('#qr-server').val();
-  $.ajax({
-    url: '../modalities/' + server + '/query',
-    type: 'POST', 
-    data: JSON.stringify(query),
-    dataType: 'json',
-    async: false,
-    error: function() {
-      alert('Error during query (C-Find)');
-    },
-    success: function(result) {
-      window.location.assign('explorer.html#query-retrieve-2?server=' + server + '&uuid=' + result['ID']);
-    }
-  });
-
-  return false;
-});
-
-
-
-function Retrieve(url)
-{
-  $.ajax({
-    url: '../system',
-    dataType: 'json',
-    async: false,
-    success: function(system) {
-      $('<div>').simpledialog2({
-        mode: 'button',
-        headerText: 'Target',
-        headerClose: true,
-        buttonPrompt: 'Enter Application Entity Title (AET):',
-        buttonInput: true,
-        buttonInputDefault: system['DicomAet'],
-        buttons : {
-          'OK': {
-            click: function () { 
-              var aet = $.mobile.sdLastInput;
-              if (aet.length == 0)
-                aet = system['DicomAet'];
-
-              $.ajax({
-                url: url,
-                type: 'POST',
-                async: true,  // Necessary to block UI
-                dataType: 'text',
-                data: aet,
-                beforeSend: function() {
-                  $.blockUI({ message: $('#info-retrieve') });
-                },
-                complete: function(s) {
-                  $.unblockUI();
-                },
-                error: function() {
-                  alert('Error during retrieve');
-                }
-              });
-            }
-          }
-        }
-      });
-    }
-  });
-}
-
-
-
-
-$('#query-retrieve-2').live('pagebeforeshow', function() {
-  if ($.mobile.pageData) {
-    var uri = '../queries/' + $.mobile.pageData.uuid + '/answers';
-
-    $.ajax({
-      url: uri,
-      dataType: 'json',
-      async: false,
-      success: function(answers) {
-        var target = $('#query-retrieve-2 ul');
-        $('li', target).remove();
-
-        for (var i = 0; i < answers.length; i++) {
-          $.ajax({
-            url: uri + '/' + answers[i] + '/content?simplify',
-            dataType: 'json',
-            async: false,
-            success: function(study) {
-              var series = '#query-retrieve-3?server=' + $.mobile.pageData.server + '&uuid=' + study['StudyInstanceUID'];
-              var info = $('<a>').attr('href', series).html(
-                ('<h3>{0} - {1}</h3>' + 
-                 '<p>Accession number: <b>{2}</b></p>' +
-                 '<p>Birth date: <b>{3}</b></p>' +
-                 '<p>Patient sex: <b>{4}</b></p>' +
-                 '<p>Study description: <b>{5}</b></p>' +
-                 '<p>Study date: <b>{6}</b></p>').format(
-                   study['PatientID'],
-                   study['PatientName'],
-                   study['AccessionNumber'],
-                   FormatDicomDate(study['PatientBirthDate']),
-                   study['PatientSex'],
-                   study['StudyDescription'],
-                   FormatDicomDate(study['StudyDate'])));
-
-              var studyUri = uri + '/' + answers[i] + '/retrieve';
-              var retrieve = $('<a>').text('Retrieve').click(function() {
-                Retrieve(studyUri);
-              });
-
-              target.append($('<li>').append(info).append(retrieve));
-            }
-          });
-        }
-
-        target.listview('refresh');
-      }
-    });
-  }
-});
-
-
-$('#query-retrieve-3').live('pagebeforeshow', function() {
-  if ($.mobile.pageData) {
-    var query = {
-      'Level' : 'Series',
-      'Query' : {
-        'Modality' : '*',
-        'ProtocolName' : '*',
-        'SeriesDescription' : '*',
-        'SeriesInstanceUID' : '*',
-        'StudyInstanceUID' : $.mobile.pageData.uuid
-      }
-    };
-
-    $.ajax({
-      url: '../modalities/' + $.mobile.pageData.server + '/query',
-      type: 'POST', 
-      data: JSON.stringify(query),
-      dataType: 'json',
-      async: false,
-      error: function() {
-        alert('Error during query (C-Find)');
-      },
-      success: function(answer) {
-        var uri = '../queries/' + answer['ID'] + '/answers';
-
-        $.ajax({
-          url: uri,
-          dataType: 'json',
-          async: false,
-          success: function(answers) {
-            
-            var target = $('#query-retrieve-3 ul');
-            $('li', target).remove();
-
-            for (var i = 0; i < answers.length; i++) {
-              $.ajax({
-                url: uri + '/' + answers[i] + '/content?simplify',
-                dataType: 'json',
-                async: false,
-                success: function(series) {
-                  var info = $('<a>').html(
-                    ('<h3>{0}</h3>'  + 
-                     '<p>Modality: <b>{1}</b></p>' +
-                     '<p>Protocol name: <b>{2}</b></p>'
-                    ).format(
-                      series['SeriesDescription'],
-                      series['Modality'],
-                      series['ProtocolName']
-                    ));
-
-                  var seriesUri = uri + '/' + answers[i] + '/retrieve';
-                  var retrieve = $('<a>').text('Retrieve').click(function() {
-                    Retrieve(seriesUri);
-                  });
-
-                  target.append($('<li>').append(info).append(retrieve));
-                }
-              });
-            }
-
-            target.listview('refresh');
-          }
-        });
-      }
-    });
-  }
-});
+function JavascriptDateToDicom(date)
+{
+  var s = date.toISOString();
+  return s.substring(0, 4) + s.substring(5, 7) + s.substring(8, 10);
+}
+
+function GenerateDicomDate(days)
+{
+  var today = new Date();
+  var other = new Date(today);
+  other.setDate(today.getDate() + days);
+  return JavascriptDateToDicom(other);
+}
+
+
+$('#query-retrieve').live('pagebeforeshow', function() {
+  $.ajax({
+    url: '../modalities',
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(modalities) {
+      var target = $('#qr-server');
+      $('option', target).remove();
+
+      for (var i = 0; i < modalities.length; i++) {
+        var option = $('<option>').text(modalities[i]);
+        target.append(option);
+      }
+
+      target.selectmenu('refresh');
+    }
+  });
+
+  var target = $('#qr-date');
+  $('option', target).remove();
+  target.append($('<option>').attr('value', '*').text('Any date'));
+  target.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-1)).text('Yesterday'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-7) + '-').text('Last 7 days'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-31) + '-').text('Last 31 days'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-31 * 3) + '-').text('Last 3 months'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-365) + '-').text('Last year'));
+  target.selectmenu('refresh');
+});
+
+
+$('#qr-echo').live('click', function() {
+  var server = $('#qr-server').val();
+  var message = 'Error: The C-Echo has failed!';
+
+  $.ajax({
+    url: '../modalities/' + server + '/echo',
+    type: 'POST', 
+    cache: false,
+    async: false,
+    success: function() {
+      message = 'The C-Echo has succeeded!';
+    }
+  });
+
+  $('<div>').simpledialog2({
+    mode: 'button',
+    headerText: 'Echo result',
+    headerClose: true,
+    buttonPrompt: message,
+    animate: false,
+    buttons : {
+      'OK': { click: function () { } }
+    }
+  });
+
+  return false;
+});
+
+
+$('#qr-submit').live('click', function() {
+  var query = {
+    'Level' : 'Study',
+    'Query' : {
+      'AccessionNumber' : '*',
+      'PatientBirthDate' : '*',
+      'PatientID' : '*',
+      'PatientName' : '*',
+      'PatientSex' : '*',
+      'StudyDate' : $('#qr-date').val(),
+      'StudyDescription' : '*'
+    }
+  };
+
+  var field = $('#qr-fields input:checked').val();
+  query['Query'][field] = $('#qr-value').val().toUpperCase();
+
+  var modalities = '';
+  $('#qr-modalities input:checked').each(function() {
+    var s = $(this).attr('name');
+    if (modalities == '')
+      modalities = s;
+    else
+      modalities += '\\' + s;
+  });
+
+  if (modalities.length > 0) {
+    query['Query']['ModalitiesInStudy'] = modalities;
+  }
+
+
+  var server = $('#qr-server').val();
+  $.ajax({
+    url: '../modalities/' + server + '/query',
+    type: 'POST', 
+    data: JSON.stringify(query),
+    dataType: 'json',
+    async: false,
+    error: function() {
+      alert('Error during query (C-Find)');
+    },
+    success: function(result) {
+      ChangePage('query-retrieve-2', {
+        'server' : server,
+        'uuid' : result['ID']
+      });
+    }
+  });
+
+  return false;
+});
+
+
+
+$('#query-retrieve-2').live('pagebeforeshow', function() {
+  if ($.mobile.pageData) {
+    var pageData = DeepCopy($.mobile.pageData);
+
+    var uri = '../queries/' + pageData.uuid + '/answers';
+
+    $.ajax({
+      url: uri,
+      dataType: 'json',
+      async: false,
+      success: function(answers) {
+        var target = $('#query-retrieve-2 ul');
+        $('li', target).remove();
+
+        for (var i = 0; i < answers.length; i++) {
+          $.ajax({
+            url: uri + '/' + answers[i] + '/content?simplify',
+            dataType: 'json',
+            async: false,
+            success: function(study) {
+              var series = '#query-retrieve-3?server=' + pageData.server + '&uuid=' + study['StudyInstanceUID'];
+
+              var content = ($('<div>')
+                             .append($('<h3>').text(study['PatientID'] + ' - ' + study['PatientName']))
+                             .append($('<p>').text('Accession number: ')
+                                     .append($('<b>').text(study['AccessionNumber'])))
+                             .append($('<p>').text('Birth date: ')
+                                     .append($('<b>').text(study['PatientBirthDate'])))
+                             .append($('<p>').text('Patient sex: ')
+                                     .append($('<b>').text(study['PatientSex'])))
+                             .append($('<p>').text('Study description: ')
+                                     .append($('<b>').text(study['StudyDescription'])))
+                             .append($('<p>').text('Study date: ')
+                                     .append($('<b>').text(FormatDicomDate(study['StudyDate'])))));
+
+              var info = $('<a>').attr('href', series).html(content);
+              
+              var answerId = answers[i];
+              var retrieve = $('<a>').text('Retrieve all study').click(function() {
+                ChangePage('query-retrieve-4', {
+                  'query' : pageData.uuid,
+                  'answer' : answerId,
+                  'server' : pageData.server
+                });
+              });
+
+              target.append($('<li>').append(info).append(retrieve));
+            }
+          });
+        }
+
+        target.listview('refresh');
+      }
+    });
+  }
+});
+
+
+$('#query-retrieve-3').live('pagebeforeshow', function() {
+  if ($.mobile.pageData) {
+    var pageData = DeepCopy($.mobile.pageData);
+
+    var query = {
+      'Level' : 'Series',
+      'Query' : {
+        'Modality' : '*',
+        'ProtocolName' : '*',
+        'SeriesDescription' : '*',
+        'SeriesInstanceUID' : '*',
+        'StudyInstanceUID' : pageData.uuid
+      }
+    };
+
+    $.ajax({
+      url: '../modalities/' + pageData.server + '/query',
+      type: 'POST', 
+      data: JSON.stringify(query),
+      dataType: 'json',
+      async: false,
+      error: function() {
+        alert('Error during query (C-Find)');
+      },
+      success: function(answer) {
+        var queryUuid = answer['ID'];
+        var uri = '../queries/' + answer['ID'] + '/answers';
+
+        $.ajax({
+          url: uri,
+          dataType: 'json',
+          async: false,
+          success: function(answers) {
+            
+            var target = $('#query-retrieve-3 ul');
+            $('li', target).remove();
+
+            for (var i = 0; i < answers.length; i++) {
+              $.ajax({
+                url: uri + '/' + answers[i] + '/content?simplify',
+                dataType: 'json',
+                async: false,
+                success: function(series) {
+                  var content = ($('<div>')
+                                 .append($('<h3>').text(series['SeriesDescription']))
+                                 .append($('<p>').text('Modality: ')
+                                         .append($('<b>').text(series['Modality'])))
+                                 .append($('<p>').text('ProtocolName: ')
+                                         .append($('<b>').text(series['ProtocolName']))));
+
+                  var info = $('<a>').html(content);
+
+                  var answerId = answers[i];
+                  info.click(function() {
+                    ChangePage('query-retrieve-4', {
+                      'query' : queryUuid,
+                      'study' : pageData.uuid,
+                      'answer' : answerId,
+                      'server' : pageData.server
+                    });
+                  });
+
+                  target.append($('<li>').attr('data-icon', 'arrow-d').append(info));
+                }
+              });
+            }
+
+            target.listview('refresh');
+          }
+        });
+      }
+    });
+  }
+});
+
+
+
+$('#query-retrieve-4').live('pagebeforeshow', function() {
+  if ($.mobile.pageData) {
+    var pageData = DeepCopy($.mobile.pageData);
+    var uri = '../queries/' + pageData.query + '/answers/' + pageData.answer + '/retrieve';
+
+    $.ajax({
+      url: '../system',
+      dataType: 'json',
+      async: false,
+      cache: false,
+      success: function(system) {
+        $('#retrieve-target').val(system['DicomAet']);
+
+        $('#retrieve-form').submit(function(event) {
+          event.preventDefault();
+
+          var aet = $('#retrieve-target').val();
+          if (aet.length == 0) {
+            aet = system['DicomAet'];
+          }
+
+          $.ajax({
+            url: uri,
+            type: 'POST',
+            async: true,  // Necessary to block UI
+            dataType: 'text',
+            data: aet,
+            beforeSend: function() {
+              $.blockUI({ message: $('#info-retrieve') });
+            },
+            complete: function(s) {
+              $.unblockUI();
+            },
+            success: function() {
+              if (pageData.study) {
+                // Go back to the list of series
+                ChangePage('query-retrieve-3', {
+                  'server' : pageData.server,
+                  'uuid' : pageData.study
+                });
+              } else {
+                // Go back to the list of studies
+                ChangePage('query-retrieve-2', {
+                  'server' : pageData.server,
+                  'uuid' : pageData.query
+                });
+              }
+            },
+            error: function() {
+              alert('Error during retrieve');
+            }
+          });
+        });
+      }
+    });
+  }
+});
--- a/OrthancServer/DatabaseWrapper.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/DatabaseWrapper.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -35,7 +36,6 @@
 
 #include "../Core/DicomFormat/DicomArray.h"
 #include "../Core/Logging.h"
-#include "../Core/Uuid.h"
 #include "EmbeddedResources.h"
 #include "ServerToolbox.h"
 
@@ -44,7 +44,6 @@
 
 namespace Orthanc
 {
-
   namespace Internals
   {
     class SignalFileDeleted : public SQLite::IScalarFunction
@@ -187,6 +186,59 @@
   }
 
 
+  void DatabaseWrapper::GetChangesInternal(std::list<ServerIndexChange>& target,
+                                           bool& done,
+                                           SQLite::Statement& s,
+                                           uint32_t maxResults)
+  {
+    target.clear();
+
+    while (target.size() < maxResults && s.Step())
+    {
+      int64_t seq = s.ColumnInt64(0);
+      ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
+      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3));
+      const std::string& date = s.ColumnString(4);
+
+      int64_t internalId = s.ColumnInt64(2);
+      std::string publicId = GetPublicId(internalId);
+
+      target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
+    }
+
+    done = !(target.size() == maxResults && s.Step());
+  }
+
+
+  void DatabaseWrapper::GetExportedResourcesInternal(std::list<ExportedResource>& target,
+                                                     bool& done,
+                                                     SQLite::Statement& s,
+                                                     uint32_t maxResults)
+  {
+    target.clear();
+
+    while (target.size() < maxResults && s.Step())
+    {
+      int64_t seq = s.ColumnInt64(0);
+      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1));
+      std::string publicId = s.ColumnString(2);
+
+      ExportedResource resource(seq, 
+                                resourceType,
+                                publicId,
+                                s.ColumnString(3),  // modality
+                                s.ColumnString(8),  // date
+                                s.ColumnString(4),  // patient ID
+                                s.ColumnString(5),  // study instance UID
+                                s.ColumnString(6),  // series instance UID
+                                s.ColumnString(7)); // sop instance UID
+
+      target.push_back(resource);
+    }
+
+    done = !(target.size() == maxResults && s.Step());
+  }
+
 
   void DatabaseWrapper::GetChildren(std::list<std::string>& childrenPublicIds,
                                     int64_t id)
@@ -256,16 +308,24 @@
   }
 
     
-  DatabaseWrapper::DatabaseWrapper(const std::string& path) : listener_(NULL), base_(db_)
+  DatabaseWrapper::DatabaseWrapper(const std::string& path) : 
+    listener_(NULL), 
+    signalRemainingAncestor_(NULL),
+    version_(0)
   {
     db_.Open(path);
   }
 
-  DatabaseWrapper::DatabaseWrapper() : listener_(NULL), base_(db_)
+
+  DatabaseWrapper::DatabaseWrapper() : 
+    listener_(NULL), 
+    signalRemainingAncestor_(NULL),
+    version_(0)
   {
     db_.OpenInMemory();
   }
 
+
   void DatabaseWrapper::Open()
   {
     db_.Execute("PRAGMA ENCODING=\"UTF-8\";");
@@ -365,10 +425,10 @@
       // consists in reconstructing the main DICOM tags information
       // (as more tags got included).
       db_.BeginTransaction();
-      Toolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Patient);
-      Toolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Study);
-      Toolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Series);
-      Toolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Instance);
+      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Patient);
+      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Study);
+      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Series);
+      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Instance);
       db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" +
                   boost::lexical_cast<std::string>(GlobalProperty_DatabaseSchemaVersion) + ";");
       db_.CommitTransaction();
@@ -394,43 +454,53 @@
   bool DatabaseWrapper::LookupParent(int64_t& parentId,
                                      int64_t resourceId)
   {
-    bool found;
-    ErrorCode error = base_.LookupParent(found, parentId, resourceId);
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT parentId FROM Resources WHERE internalId=?");
+    s.BindInt64(0, resourceId);
 
-    if (error != ErrorCode_Success)
+    if (!s.Step())
     {
-      throw OrthancException(error);
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    if (s.ColumnIsNull(0))
+    {
+      return false;
     }
     else
     {
-      return found;
+      parentId = s.ColumnInt(0);
+      return true;
     }
   }
 
 
   ResourceType DatabaseWrapper::GetResourceType(int64_t resourceId)
   {
-    ResourceType result;
-    ErrorCode code = base_.GetResourceType(result, resourceId);
-
-    if (code == ErrorCode_Success)
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT resourceType FROM Resources WHERE internalId=?");
+    s.BindInt64(0, resourceId);
+    
+    if (s.Step())
     {
-      return result;
+      return static_cast<ResourceType>(s.ColumnInt(0));
     }
     else
-    {
-      throw OrthancException(code);
+    { 
+      throw OrthancException(ErrorCode_UnknownResource);
     }
   }
 
 
   std::string DatabaseWrapper::GetPublicId(int64_t resourceId)
   {
-    std::string id;
-
-    if (base_.GetPublicId(id, resourceId))
-    {
-      return id;
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT publicId FROM Resources WHERE internalId=?");
+    s.BindInt64(0, resourceId);
+    
+    if (s.Step())
+    { 
+      return s.ColumnString(0);
     }
     else
     {
@@ -444,23 +514,18 @@
                                    int64_t since,
                                    uint32_t maxResults)
   {
-    ErrorCode code = base_.GetChanges(target, done, since, maxResults);
-
-    if (code != ErrorCode_Success)
-    {
-      throw OrthancException(code);
-    }
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
+    s.BindInt64(0, since);
+    s.BindInt(1, maxResults + 1);
+    GetChangesInternal(target, done, s, maxResults);
   }
 
 
   void DatabaseWrapper::GetLastChange(std::list<ServerIndexChange>& target /*out*/)
   {
-    ErrorCode code = base_.GetLastChange(target);
-
-    if (code != ErrorCode_Success)
-    {
-      throw OrthancException(code);
-    }
+    bool done;  // Ignored
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
+    GetChangesInternal(target, done, s, 1);
   }
 
 
@@ -479,4 +544,588 @@
     }
   }
 
+
+  void DatabaseWrapper::SetGlobalProperty(GlobalProperty property,
+                                          const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)");
+    s.BindInt(0, property);
+    s.BindString(1, value);
+    s.Run();
+  }
+
+
+  bool DatabaseWrapper::LookupGlobalProperty(std::string& target,
+                                             GlobalProperty property)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT value FROM GlobalProperties WHERE property=?");
+    s.BindInt(0, property);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      target = s.ColumnString(0);
+      return true;
+    }
+  }
+
+
+  int64_t DatabaseWrapper::CreateResource(const std::string& publicId,
+                                          ResourceType type)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)");
+    s.BindInt(0, type);
+    s.BindString(1, publicId);
+    s.Run();
+    return db_.GetLastInsertRowId();
+  }
+
+
+  bool DatabaseWrapper::LookupResource(int64_t& id,
+                                       ResourceType& type,
+                                       const std::string& publicId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT internalId, resourceType FROM Resources WHERE publicId=?");
+    s.BindString(0, publicId);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      id = s.ColumnInt(0);
+      type = static_cast<ResourceType>(s.ColumnInt(1));
+
+      // Check whether there is a single resource with this public id
+      assert(!s.Step());
+
+      return true;
+    }
+  }
+
+
+  void DatabaseWrapper::AttachChild(int64_t parent,
+                                    int64_t child)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?");
+    s.BindInt64(0, parent);
+    s.BindInt64(1, child);
+    s.Run();
+  }
+
+
+  void DatabaseWrapper::SetMetadata(int64_t id,
+                                    MetadataType type,
+                                    const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)");
+    s.BindInt64(0, id);
+    s.BindInt(1, type);
+    s.BindString(2, value);
+    s.Run();
+  }
+
+
+  void DatabaseWrapper::DeleteMetadata(int64_t id,
+                                       MetadataType type)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?");
+    s.BindInt64(0, id);
+    s.BindInt(1, type);
+    s.Run();
+  }
+
+
+  bool DatabaseWrapper::LookupMetadata(std::string& target,
+                                       int64_t id,
+                                       MetadataType type)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT value FROM Metadata WHERE id=? AND type=?");
+    s.BindInt64(0, id);
+    s.BindInt(1, type);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      target = s.ColumnString(0);
+      return true;
+    }
+  }
+
+
+  void DatabaseWrapper::ListAvailableMetadata(std::list<MetadataType>& target,
+                                              int64_t id)
+  {
+    target.clear();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type FROM Metadata WHERE id=?");
+    s.BindInt64(0, id);
+
+    while (s.Step())
+    {
+      target.push_back(static_cast<MetadataType>(s.ColumnInt(0)));
+    }
+  }
+
+
+  void DatabaseWrapper::AddAttachment(int64_t id,
+                                      const FileInfo& attachment)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
+    s.BindInt64(0, id);
+    s.BindInt(1, attachment.GetContentType());
+    s.BindString(2, attachment.GetUuid());
+    s.BindInt64(3, attachment.GetCompressedSize());
+    s.BindInt64(4, attachment.GetUncompressedSize());
+    s.BindInt(5, attachment.GetCompressionType());
+    s.BindString(6, attachment.GetUncompressedMD5());
+    s.BindString(7, attachment.GetCompressedMD5());
+    s.Run();
+  }
+
+
+  void DatabaseWrapper::DeleteAttachment(int64_t id,
+                                         FileContentType attachment)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?");
+    s.BindInt64(0, id);
+    s.BindInt(1, attachment);
+    s.Run();
+  }
+
+
+  void DatabaseWrapper::ListAvailableAttachments(std::list<FileContentType>& target,
+                                                 int64_t id)
+  {
+    target.clear();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT fileType FROM AttachedFiles WHERE id=?");
+    s.BindInt64(0, id);
+
+    while (s.Step())
+    {
+      target.push_back(static_cast<FileContentType>(s.ColumnInt(0)));
+    }
+  }
+
+  bool DatabaseWrapper::LookupAttachment(FileInfo& attachment,
+                                         int64_t id,
+                                         FileContentType contentType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT uuid, uncompressedSize, compressionType, compressedSize, "
+                        "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?");
+    s.BindInt64(0, id);
+    s.BindInt(1, contentType);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      attachment = FileInfo(s.ColumnString(0),
+                            contentType,
+                            s.ColumnInt64(1),
+                            s.ColumnString(4),
+                            static_cast<CompressionType>(s.ColumnInt(2)),
+                            s.ColumnInt64(3),
+                            s.ColumnString(5));
+      return true;
+    }
+  }
+
+
+  void DatabaseWrapper::ClearMainDicomTags(int64_t id)
+  {
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?");
+      s.BindInt64(0, id);
+      s.Run();
+    }
+
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?");
+      s.BindInt64(0, id);
+      s.Run();
+    }
+  }
+
+
+  void DatabaseWrapper::SetMainDicomTag(int64_t id,
+                                        const DicomTag& tag,
+                                        const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
+    s.BindInt64(0, id);
+    s.BindInt(1, tag.GetGroup());
+    s.BindInt(2, tag.GetElement());
+    s.BindString(3, value);
+    s.Run();
+  }
+
+
+  void DatabaseWrapper::SetIdentifierTag(int64_t id,
+                                         const DicomTag& tag,
+                                         const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)");
+    s.BindInt64(0, id);
+    s.BindInt(1, tag.GetGroup());
+    s.BindInt(2, tag.GetElement());
+    s.BindString(3, value);
+    s.Run();
+  }
+
+
+  void DatabaseWrapper::GetMainDicomTags(DicomMap& map,
+                                         int64_t id)
+  {
+    map.Clear();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?");
+    s.BindInt64(0, id);
+    while (s.Step())
+    {
+      map.SetValue(s.ColumnInt(1),
+                   s.ColumnInt(2),
+                   s.ColumnString(3), false);
+    }
+  }
+
+
+  void DatabaseWrapper::GetChildrenPublicId(std::list<std::string>& target,
+                                            int64_t id)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b  "
+                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
+    s.BindInt64(0, id);
+
+    target.clear();
+
+    while (s.Step())
+    {
+      target.push_back(s.ColumnString(0));
+    }
+  }
+
+
+  void DatabaseWrapper::GetChildrenInternalId(std::list<int64_t>& target,
+                                              int64_t id)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b  "
+                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
+    s.BindInt64(0, id);
+
+    target.clear();
+
+    while (s.Step())
+    {
+      target.push_back(s.ColumnInt64(0));
+    }
+  }
+
+
+  void DatabaseWrapper::LogChange(int64_t internalId,
+                                  const ServerIndexChange& change)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)");
+    s.BindInt(0, change.GetChangeType());
+    s.BindInt64(1, internalId);
+    s.BindInt(2, change.GetResourceType());
+    s.BindString(3, change.GetDate());
+    s.Run();
+  }
+
+
+  void DatabaseWrapper::LogExportedResource(const ExportedResource& resource)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)");
+
+    s.BindInt(0, resource.GetResourceType());
+    s.BindString(1, resource.GetPublicId());
+    s.BindString(2, resource.GetModality());
+    s.BindString(3, resource.GetPatientId());
+    s.BindString(4, resource.GetStudyInstanceUid());
+    s.BindString(5, resource.GetSeriesInstanceUid());
+    s.BindString(6, resource.GetSopInstanceUid());
+    s.BindString(7, resource.GetDate());
+    s.Run();      
+  }
+
+
+  void DatabaseWrapper::GetExportedResources(std::list<ExportedResource>& target,
+                                             bool& done,
+                                             int64_t since,
+                                             uint32_t maxResults)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?");
+    s.BindInt64(0, since);
+    s.BindInt(1, maxResults + 1);
+    GetExportedResourcesInternal(target, done, s, maxResults);
+  }
+
+    
+  void DatabaseWrapper::GetLastExportedResource(std::list<ExportedResource>& target)
+  {
+    bool done;  // Ignored
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1");
+    GetExportedResourcesInternal(target, done, s, 1);
+  }
+
+    
+  uint64_t DatabaseWrapper::GetTotalCompressedSize()
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles");
+    s.Run();
+    return static_cast<uint64_t>(s.ColumnInt64(0));
+  }
+
+    
+  uint64_t DatabaseWrapper::GetTotalUncompressedSize()
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
+    s.Run();
+    return static_cast<uint64_t>(s.ColumnInt64(0));
+  }
+
+
+  uint64_t DatabaseWrapper::GetResourceCount(ResourceType resourceType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT COUNT(*) FROM Resources WHERE resourceType=?");
+    s.BindInt(0, resourceType);
+    
+    if (!s.Step())
+    {
+      return 0;
+    }
+    else
+    {
+      int64_t c = s.ColumnInt(0);
+      assert(!s.Step());
+      return c;
+    }
+  }
+
+
+  void DatabaseWrapper::GetAllInternalIds(std::list<int64_t>& target,
+                                          ResourceType resourceType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId FROM Resources WHERE resourceType=?");
+    s.BindInt(0, resourceType);
+
+    target.clear();
+    while (s.Step())
+    {
+      target.push_back(s.ColumnInt64(0));
+    }
+  }
+
+
+  void DatabaseWrapper::GetAllPublicIds(std::list<std::string>& target,
+                                        ResourceType resourceType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?");
+    s.BindInt(0, resourceType);
+
+    target.clear();
+    while (s.Step())
+    {
+      target.push_back(s.ColumnString(0));
+    }
+  }
+
+
+  void DatabaseWrapper::GetAllPublicIds(std::list<std::string>& target,
+                                        ResourceType resourceType,
+                                        size_t since,
+                                        size_t limit)
+  {
+    if (limit == 0)
+    {
+      target.clear();
+      return;
+    }
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                        "SELECT publicId FROM Resources WHERE "
+                        "resourceType=? LIMIT ? OFFSET ?");
+    s.BindInt(0, resourceType);
+    s.BindInt64(1, limit);
+    s.BindInt64(2, since);
+
+    target.clear();
+    while (s.Step())
+    {
+      target.push_back(s.ColumnString(0));
+    }
+  }
+
+
+  bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                        "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1");
+   
+    if (!s.Step())
+    {
+      // No patient remaining or all the patients are protected
+      return false;
+    }
+    else
+    {
+      internalId = s.ColumnInt(0);
+      return true;
+    }    
+  }
+
+
+  bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId,
+                                               int64_t patientIdToAvoid)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                        "SELECT patientId FROM PatientRecyclingOrder "
+                        "WHERE patientId != ? ORDER BY seq ASC LIMIT 1");
+    s.BindInt64(0, patientIdToAvoid);
+
+    if (!s.Step())
+    {
+      // No patient remaining or all the patients are protected
+      return false;
+    }
+    else
+    {
+      internalId = s.ColumnInt(0);
+      return true;
+    }   
+  }
+
+
+  bool DatabaseWrapper::IsProtectedPatient(int64_t internalId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                        "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?");
+    s.BindInt64(0, internalId);
+    return !s.Step();
+  }
+
+
+  void DatabaseWrapper::SetProtectedPatient(int64_t internalId, 
+                                            bool isProtected)
+  {
+    if (isProtected)
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
+      s.BindInt64(0, internalId);
+      s.Run();
+    }
+    else if (IsProtectedPatient(internalId))
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
+      s.BindInt64(0, internalId);
+      s.Run();
+    }
+    else
+    {
+      // Nothing to do: The patient is already unprotected
+    }
+  }
+
+
+  bool DatabaseWrapper::IsExistingResource(int64_t internalId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT * FROM Resources WHERE internalId=?");
+    s.BindInt64(0, internalId);
+    return s.Step();
+  }
+
+
+  void DatabaseWrapper::LookupIdentifier(std::list<int64_t>& target,
+                                         ResourceType level,
+                                         const DicomTag& tag,
+                                         IdentifierConstraintType type,
+                                         const std::string& value)
+  {
+    static const char* COMMON = ("SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE "
+                                 "d.id = r.internalId AND r.resourceType=? AND "
+                                 "d.tagGroup=? AND d.tagElement=? AND ");
+
+    std::auto_ptr<SQLite::Statement> s;
+
+    switch (type)
+    {
+      case IdentifierConstraintType_GreaterOrEqual:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value>=?"));
+        break;
+
+      case IdentifierConstraintType_SmallerOrEqual:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value<=?"));
+        break;
+
+      case IdentifierConstraintType_Wildcard:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value GLOB ?"));
+        break;
+
+      case IdentifierConstraintType_Equal:
+      default:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value=?"));
+        break;
+    }
+
+    assert(s.get() != NULL);
+
+    s->BindInt(0, level);
+    s->BindInt(1, tag.GetGroup());
+    s->BindInt(2, tag.GetElement());
+    s->BindString(3, value);
+
+    target.clear();
+
+    while (s->Step())
+    {
+      target.push_back(s->ColumnInt64(0));
+    }    
+  }
+
+
+  void DatabaseWrapper::LookupIdentifierRange(std::list<int64_t>& target,
+                                              ResourceType level,
+                                              const DicomTag& tag,
+                                              const std::string& start,
+                                              const std::string& end)
+  {
+    SQLite::Statement statement(db_, SQLITE_FROM_HERE,
+                                "SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE "
+                                "d.id = r.internalId AND r.resourceType=? AND "
+                                "d.tagGroup=? AND d.tagElement=? AND d.value>=? AND d.value<=?");
+
+    statement.BindInt(0, level);
+    statement.BindInt(1, tag.GetGroup());
+    statement.BindInt(2, tag.GetElement());
+    statement.BindString(3, start);
+    statement.BindString(4, end);
+
+    target.clear();
+
+    while (statement.Step())
+    {
+      target.push_back(statement.ColumnInt64(0));
+    }    
+  }
 }
--- a/OrthancServer/DatabaseWrapper.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/DatabaseWrapper.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -36,7 +37,6 @@
 
 #include "../Core/SQLite/Connection.h"
 #include "../Core/SQLite/Transaction.h"
-#include "DatabaseWrapperBase.h"
 
 namespace Orthanc
 {
@@ -55,10 +55,19 @@
   private:
     IDatabaseListener* listener_;
     SQLite::Connection db_;
-    DatabaseWrapperBase base_;
     Internals::SignalRemainingAncestor* signalRemainingAncestor_;
     unsigned int version_;
 
+    void GetChangesInternal(std::list<ServerIndexChange>& target,
+                            bool& done,
+                            SQLite::Statement& s,
+                            uint32_t maxResults);
+
+    void GetExportedResourcesInternal(std::list<ExportedResource>& target,
+                                      bool& done,
+                                      SQLite::Statement& s,
+                                      uint32_t maxResults);
+
     void ClearTable(const std::string& tableName);
 
   public:
@@ -75,31 +84,6 @@
 
     virtual void SetListener(IDatabaseListener& listener);
 
-    virtual void SetGlobalProperty(GlobalProperty property,
-                                   const std::string& value)
-    {
-      base_.SetGlobalProperty(property, value);
-    }
-
-    virtual bool LookupGlobalProperty(std::string& target,
-                                      GlobalProperty property)
-    {
-      return base_.LookupGlobalProperty(target, property);
-    }
-
-    virtual int64_t CreateResource(const std::string& publicId,
-                                   ResourceType type)
-    {
-      return base_.CreateResource(publicId, type);
-    }
-
-    virtual bool LookupResource(int64_t& id,
-                                ResourceType& type,
-                                const std::string& publicId)
-    {
-      return base_.LookupResource(id, type, publicId);
-    }
-
     virtual bool LookupParent(int64_t& parentId,
                               int64_t resourceId);
 
@@ -107,108 +91,8 @@
 
     virtual ResourceType GetResourceType(int64_t resourceId);
 
-    virtual void AttachChild(int64_t parent,
-                             int64_t child)
-    {
-      base_.AttachChild(parent, child);
-    }
-
     virtual void DeleteResource(int64_t id);
 
-    virtual void SetMetadata(int64_t id,
-                             MetadataType type,
-                             const std::string& value)
-    {
-      base_.SetMetadata(id, type, value);
-    }
-
-    virtual void DeleteMetadata(int64_t id,
-                                MetadataType type)
-    {
-      base_.DeleteMetadata(id, type);
-    }
-
-    virtual bool LookupMetadata(std::string& target,
-                                int64_t id,
-                                MetadataType type)
-    {
-      return base_.LookupMetadata(target, id, type);
-    }
-
-    virtual void ListAvailableMetadata(std::list<MetadataType>& target,
-                                       int64_t id)
-    {
-      base_.ListAvailableMetadata(target, id);
-    }
-
-    virtual void AddAttachment(int64_t id,
-                               const FileInfo& attachment)
-    {
-      base_.AddAttachment(id, attachment);
-    }
-
-    virtual void DeleteAttachment(int64_t id,
-                                  FileContentType attachment)
-    {
-      base_.DeleteAttachment(id, attachment);
-    }
-
-    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
-                                          int64_t id)
-    {
-      return base_.ListAvailableAttachments(target, id);
-    }
-
-    virtual bool LookupAttachment(FileInfo& attachment,
-                                  int64_t id,
-                                  FileContentType contentType)
-    {
-      return base_.LookupAttachment(attachment, id, contentType);
-    }
-
-    virtual void ClearMainDicomTags(int64_t id)
-    {
-      base_.ClearMainDicomTags(id);
-    }
-
-    virtual void SetMainDicomTag(int64_t id,
-                                 const DicomTag& tag,
-                                 const std::string& value)
-    {
-      base_.SetMainDicomTag(id, tag, value);
-    }
-
-    virtual void SetIdentifierTag(int64_t id,
-                                 const DicomTag& tag,
-                                 const std::string& value)
-    {
-      base_.SetIdentifierTag(id, tag, value);
-    }
-
-    virtual void GetMainDicomTags(DicomMap& map,
-                                  int64_t id)
-    {
-      base_.GetMainDicomTags(map, id);
-    }
-
-    virtual void GetChildrenPublicId(std::list<std::string>& target,
-                                     int64_t id)
-    {
-      base_.GetChildrenPublicId(target, id);
-    }
-
-    virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                       int64_t id)
-    {
-      base_.GetChildrenInternalId(target, id);
-    }
-
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change)
-    {
-      base_.LogChange(internalId, change);
-    }
-
     virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                             bool& done /*out*/,
                             int64_t since,
@@ -216,81 +100,6 @@
 
     virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/);
 
-    virtual void LogExportedResource(const ExportedResource& resource)
-    {
-      base_.LogExportedResource(resource);
-    }
-    
-    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                      bool& done /*out*/,
-                                      int64_t since,
-                                      uint32_t maxResults)
-    {
-      base_.GetExportedResources(target, done, since, maxResults);
-    }
-
-    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/)
-    {
-      base_.GetLastExportedResource(target);
-    }
-
-    virtual uint64_t GetTotalCompressedSize()
-    {
-      return base_.GetTotalCompressedSize();
-    }
-    
-    virtual uint64_t GetTotalUncompressedSize()
-    {
-      return base_.GetTotalUncompressedSize();
-    }
-
-    virtual uint64_t GetResourceCount(ResourceType resourceType)
-    {
-      return base_.GetResourceCount(resourceType);
-    }
-
-    virtual void GetAllInternalIds(std::list<int64_t>& target,
-                                   ResourceType resourceType)
-    {
-      base_.GetAllInternalIds(target, resourceType);
-    }
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType)
-    {
-      base_.GetAllPublicIds(target, resourceType);
-    }
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit)
-    {
-      base_.GetAllPublicIds(target, resourceType, since, limit);
-    }
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId)
-    {
-      return base_.SelectPatientToRecycle(internalId);
-    }
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId,
-                                        int64_t patientIdToAvoid)
-    {
-      return base_.SelectPatientToRecycle(internalId, patientIdToAvoid);
-    }
-
-    virtual bool IsProtectedPatient(int64_t internalId)
-    {
-      return base_.IsProtectedPatient(internalId);
-    }
-
-    virtual void SetProtectedPatient(int64_t internalId, 
-                                     bool isProtected)
-    {
-      base_.SetProtectedPatient(internalId, isProtected);
-    }
-
     virtual SQLite::ITransaction* StartTransaction()
     {
       return new SQLite::Transaction(db_);
@@ -316,20 +125,6 @@
       ClearTable("ExportedResources");
     }
 
-    virtual bool IsExistingResource(int64_t internalId)
-    {
-      return base_.IsExistingResource(internalId);
-    }
-
-    virtual void LookupIdentifier(std::list<int64_t>& result,
-                                  ResourceType level,
-                                  const DicomTag& tag,
-                                  IdentifierConstraintType type,
-                                  const std::string& value)
-    {
-      base_.LookupIdentifier(result, level, tag, type, value);
-    }
-
     virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
                                 int64_t id);
 
@@ -342,7 +137,6 @@
                          IStorageArea& storageArea);
 
 
-
     /**
      * The methods declared below are for unit testing only!
      **/
@@ -360,5 +154,127 @@
     bool GetParentPublicId(std::string& target,
                            int64_t id);
 
+
+
+    /**
+     * Until Orthanc 1.4.0, the methods below were part of the
+     * "DatabaseWrapperBase" class, that is now placed in the
+     * graveyard.
+     **/
+
+    virtual void SetGlobalProperty(GlobalProperty property,
+                                   const std::string& value);
+
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property);
+
+    virtual int64_t CreateResource(const std::string& publicId,
+                                   ResourceType type);
+
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId);
+
+    virtual void AttachChild(int64_t parent,
+                             int64_t child);
+
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value);
+
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type);
+
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t id,
+                                MetadataType type);
+
+    virtual void ListAvailableMetadata(std::list<MetadataType>& target,
+                                       int64_t id);
+
+    virtual void AddAttachment(int64_t id,
+                               const FileInfo& attachment);
+
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment);
+
+    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
+                                          int64_t id);
+
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t id,
+                                  FileContentType contentType);
+
+    virtual void ClearMainDicomTags(int64_t id);
+
+    virtual void SetMainDicomTag(int64_t id,
+                                 const DicomTag& tag,
+                                 const std::string& value);
+
+    virtual void SetIdentifierTag(int64_t id,
+                                  const DicomTag& tag,
+                                  const std::string& value);
+
+    virtual void GetMainDicomTags(DicomMap& map,
+                                  int64_t id);
+
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id);
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id);
+
+    virtual void LogChange(int64_t internalId,
+                           const ServerIndexChange& change);
+
+    virtual void LogExportedResource(const ExportedResource& resource);
+    
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults);
+
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/);
+
+    virtual uint64_t GetTotalCompressedSize();
+    
+    virtual uint64_t GetTotalUncompressedSize();
+
+    virtual uint64_t GetResourceCount(ResourceType resourceType);
+
+    virtual void GetAllInternalIds(std::list<int64_t>& target,
+                                   ResourceType resourceType);
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType);
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType,
+                                 size_t since,
+                                 size_t limit);
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId);
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid);
+
+    virtual bool IsProtectedPatient(int64_t internalId);
+
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected);
+
+    virtual bool IsExistingResource(int64_t internalId);
+
+    virtual void LookupIdentifier(std::list<int64_t>& result,
+                                  ResourceType level,
+                                  const DicomTag& tag,
+                                  IdentifierConstraintType type,
+                                  const std::string& value);
+
+    virtual void LookupIdentifierRange(std::list<int64_t>& result,
+                                       ResourceType level,
+                                       const DicomTag& tag,
+                                       const std::string& start,
+                                       const std::string& end);
   };
 }
--- a/OrthancServer/DatabaseWrapperBase.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,730 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "PrecompiledHeadersServer.h"
-#include "DatabaseWrapperBase.h"
-
-#include <stdio.h>
-#include <memory>
-
-namespace Orthanc
-{
-  void DatabaseWrapperBase::SetGlobalProperty(GlobalProperty property,
-                                              const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)");
-    s.BindInt(0, property);
-    s.BindString(1, value);
-    s.Run();
-  }
-
-  bool DatabaseWrapperBase::LookupGlobalProperty(std::string& target,
-                                                 GlobalProperty property)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT value FROM GlobalProperties WHERE property=?");
-    s.BindInt(0, property);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      target = s.ColumnString(0);
-      return true;
-    }
-  }
-
-  int64_t DatabaseWrapperBase::CreateResource(const std::string& publicId,
-                                              ResourceType type)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)");
-    s.BindInt(0, type);
-    s.BindString(1, publicId);
-    s.Run();
-    return db_.GetLastInsertRowId();
-  }
-
-  bool DatabaseWrapperBase::LookupResource(int64_t& id,
-                                           ResourceType& type,
-                                           const std::string& publicId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT internalId, resourceType FROM Resources WHERE publicId=?");
-    s.BindString(0, publicId);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      id = s.ColumnInt(0);
-      type = static_cast<ResourceType>(s.ColumnInt(1));
-
-      // Check whether there is a single resource with this public id
-      assert(!s.Step());
-
-      return true;
-    }
-  }
-
-  ErrorCode DatabaseWrapperBase::LookupParent(bool& found,
-                                              int64_t& parentId,
-                                              int64_t resourceId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT parentId FROM Resources WHERE internalId=?");
-    s.BindInt64(0, resourceId);
-
-    if (!s.Step())
-    {
-      return ErrorCode_UnknownResource;
-    }
-
-    if (s.ColumnIsNull(0))
-    {
-      found = false;
-    }
-    else
-    {
-      found = true;
-      parentId = s.ColumnInt(0);
-    }
-
-    return ErrorCode_Success;
-  }
-
-  bool DatabaseWrapperBase::GetPublicId(std::string& result,
-                                        int64_t resourceId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT publicId FROM Resources WHERE internalId=?");
-    s.BindInt64(0, resourceId);
-    
-    if (!s.Step())
-    { 
-      return false;
-    }
-    else
-    {
-      result = s.ColumnString(0);
-      return true;
-    }
-  }
-
-
-  ErrorCode DatabaseWrapperBase::GetResourceType(ResourceType& result,
-                                                 int64_t resourceId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT resourceType FROM Resources WHERE internalId=?");
-    s.BindInt64(0, resourceId);
-    
-    if (s.Step())
-    {
-      result = static_cast<ResourceType>(s.ColumnInt(0));
-      return ErrorCode_Success;
-    }
-    else
-    { 
-      return ErrorCode_UnknownResource;
-    }
-  }
-
-
-  void DatabaseWrapperBase::AttachChild(int64_t parent,
-                                        int64_t child)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?");
-    s.BindInt64(0, parent);
-    s.BindInt64(1, child);
-    s.Run();
-  }
-
-
-  void DatabaseWrapperBase::SetMetadata(int64_t id,
-                                        MetadataType type,
-                                        const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, type);
-    s.BindString(2, value);
-    s.Run();
-  }
-
-  void DatabaseWrapperBase::DeleteMetadata(int64_t id,
-                                           MetadataType type)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, type);
-    s.Run();
-  }
-
-  bool DatabaseWrapperBase::LookupMetadata(std::string& target,
-                                           int64_t id,
-                                           MetadataType type)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT value FROM Metadata WHERE id=? AND type=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, type);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      target = s.ColumnString(0);
-      return true;
-    }
-  }
-
-  void DatabaseWrapperBase::ListAvailableMetadata(std::list<MetadataType>& target,
-                                                  int64_t id)
-  {
-    target.clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type FROM Metadata WHERE id=?");
-    s.BindInt64(0, id);
-
-    while (s.Step())
-    {
-      target.push_back(static_cast<MetadataType>(s.ColumnInt(0)));
-    }
-  }
-
-
-  void DatabaseWrapperBase::AddAttachment(int64_t id,
-                                          const FileInfo& attachment)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, attachment.GetContentType());
-    s.BindString(2, attachment.GetUuid());
-    s.BindInt64(3, attachment.GetCompressedSize());
-    s.BindInt64(4, attachment.GetUncompressedSize());
-    s.BindInt(5, attachment.GetCompressionType());
-    s.BindString(6, attachment.GetUncompressedMD5());
-    s.BindString(7, attachment.GetCompressedMD5());
-    s.Run();
-  }
-
-
-  void DatabaseWrapperBase::DeleteAttachment(int64_t id,
-                                             FileContentType attachment)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, attachment);
-    s.Run();
-  }
-
-
-
-  void DatabaseWrapperBase::ListAvailableAttachments(std::list<FileContentType>& target,
-                                                     int64_t id)
-  {
-    target.clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT fileType FROM AttachedFiles WHERE id=?");
-    s.BindInt64(0, id);
-
-    while (s.Step())
-    {
-      target.push_back(static_cast<FileContentType>(s.ColumnInt(0)));
-    }
-  }
-
-  bool DatabaseWrapperBase::LookupAttachment(FileInfo& attachment,
-                                             int64_t id,
-                                             FileContentType contentType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT uuid, uncompressedSize, compressionType, compressedSize, uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, contentType);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      attachment = FileInfo(s.ColumnString(0),
-                            contentType,
-                            s.ColumnInt64(1),
-                            s.ColumnString(4),
-                            static_cast<CompressionType>(s.ColumnInt(2)),
-                            s.ColumnInt64(3),
-                            s.ColumnString(5));
-      return true;
-    }
-  }
-
-
-  void DatabaseWrapperBase::ClearMainDicomTags(int64_t id)
-  {
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?");
-      s.BindInt64(0, id);
-      s.Run();
-    }
-
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?");
-      s.BindInt64(0, id);
-      s.Run();
-    }
-  }
-
-
-  void DatabaseWrapperBase::SetMainDicomTag(int64_t id,
-                                            const DicomTag& tag,
-                                            const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, tag.GetGroup());
-    s.BindInt(2, tag.GetElement());
-    s.BindString(3, value);
-    s.Run();
-  }
-
-
-  void DatabaseWrapperBase::SetIdentifierTag(int64_t id,
-                                             const DicomTag& tag,
-                                             const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, tag.GetGroup());
-    s.BindInt(2, tag.GetElement());
-    s.BindString(3, value);
-    s.Run();
-  }
-
-
-  void DatabaseWrapperBase::GetMainDicomTags(DicomMap& map,
-                                             int64_t id)
-  {
-    map.Clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?");
-    s.BindInt64(0, id);
-    while (s.Step())
-    {
-      map.SetValue(s.ColumnInt(1),
-                   s.ColumnInt(2),
-                   s.ColumnString(3));
-    }
-  }
-
-
-
-  void DatabaseWrapperBase::GetChildrenPublicId(std::list<std::string>& target,
-                                                int64_t id)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b  "
-                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
-    s.BindInt64(0, id);
-
-    target.clear();
-
-    while (s.Step())
-    {
-      target.push_back(s.ColumnString(0));
-    }
-  }
-
-
-  void DatabaseWrapperBase::GetChildrenInternalId(std::list<int64_t>& target,
-                                                  int64_t id)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b  "
-                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
-    s.BindInt64(0, id);
-
-    target.clear();
-
-    while (s.Step())
-    {
-      target.push_back(s.ColumnInt64(0));
-    }
-  }
-
-
-  void DatabaseWrapperBase::LogChange(int64_t internalId,
-                                      const ServerIndexChange& change)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)");
-    s.BindInt(0, change.GetChangeType());
-    s.BindInt64(1, internalId);
-    s.BindInt(2, change.GetResourceType());
-    s.BindString(3, change.GetDate());
-    s.Run();
-  }
-
-
-  ErrorCode DatabaseWrapperBase::GetChangesInternal(std::list<ServerIndexChange>& target,
-                                                    bool& done,
-                                                    SQLite::Statement& s,
-                                                    uint32_t maxResults)
-  {
-    target.clear();
-
-    while (target.size() < maxResults && s.Step())
-    {
-      int64_t seq = s.ColumnInt64(0);
-      ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
-      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3));
-      const std::string& date = s.ColumnString(4);
-
-      int64_t internalId = s.ColumnInt64(2);
-      std::string publicId;
-      if (!GetPublicId(publicId, internalId))
-      {
-        return ErrorCode_UnknownResource;
-      }
-
-      target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
-    }
-
-    done = !(target.size() == maxResults && s.Step());
-    return ErrorCode_Success;
-  }
-
-
-  ErrorCode DatabaseWrapperBase::GetChanges(std::list<ServerIndexChange>& target,
-                                            bool& done,
-                                            int64_t since,
-                                            uint32_t maxResults)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
-    s.BindInt64(0, since);
-    s.BindInt(1, maxResults + 1);
-    return GetChangesInternal(target, done, s, maxResults);
-  }
-
-  ErrorCode DatabaseWrapperBase::GetLastChange(std::list<ServerIndexChange>& target)
-  {
-    bool done;  // Ignored
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
-    return GetChangesInternal(target, done, s, 1);
-  }
-
-
-  void DatabaseWrapperBase::LogExportedResource(const ExportedResource& resource)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)");
-
-    s.BindInt(0, resource.GetResourceType());
-    s.BindString(1, resource.GetPublicId());
-    s.BindString(2, resource.GetModality());
-    s.BindString(3, resource.GetPatientId());
-    s.BindString(4, resource.GetStudyInstanceUid());
-    s.BindString(5, resource.GetSeriesInstanceUid());
-    s.BindString(6, resource.GetSopInstanceUid());
-    s.BindString(7, resource.GetDate());
-    s.Run();      
-  }
-
-
-  void DatabaseWrapperBase::GetExportedResourcesInternal(std::list<ExportedResource>& target,
-                                                         bool& done,
-                                                         SQLite::Statement& s,
-                                                         uint32_t maxResults)
-  {
-    target.clear();
-
-    while (target.size() < maxResults && s.Step())
-    {
-      int64_t seq = s.ColumnInt64(0);
-      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1));
-      std::string publicId = s.ColumnString(2);
-
-      ExportedResource resource(seq, 
-                                resourceType,
-                                publicId,
-                                s.ColumnString(3),  // modality
-                                s.ColumnString(8),  // date
-                                s.ColumnString(4),  // patient ID
-                                s.ColumnString(5),  // study instance UID
-                                s.ColumnString(6),  // series instance UID
-                                s.ColumnString(7)); // sop instance UID
-
-      target.push_back(resource);
-    }
-
-    done = !(target.size() == maxResults && s.Step());
-  }
-
-
-  void DatabaseWrapperBase::GetExportedResources(std::list<ExportedResource>& target,
-                                                 bool& done,
-                                                 int64_t since,
-                                                 uint32_t maxResults)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?");
-    s.BindInt64(0, since);
-    s.BindInt(1, maxResults + 1);
-    GetExportedResourcesInternal(target, done, s, maxResults);
-  }
-
-    
-  void DatabaseWrapperBase::GetLastExportedResource(std::list<ExportedResource>& target)
-  {
-    bool done;  // Ignored
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1");
-    GetExportedResourcesInternal(target, done, s, 1);
-  }
-
-
-    
-  uint64_t DatabaseWrapperBase::GetTotalCompressedSize()
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles");
-    s.Run();
-    return static_cast<uint64_t>(s.ColumnInt64(0));
-  }
-
-    
-  uint64_t DatabaseWrapperBase::GetTotalUncompressedSize()
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
-    s.Run();
-    return static_cast<uint64_t>(s.ColumnInt64(0));
-  }
-
-  void DatabaseWrapperBase::GetAllInternalIds(std::list<int64_t>& target,
-                                              ResourceType resourceType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId FROM Resources WHERE resourceType=?");
-    s.BindInt(0, resourceType);
-
-    target.clear();
-    while (s.Step())
-    {
-      target.push_back(s.ColumnInt64(0));
-    }
-  }
-
-
-  void DatabaseWrapperBase::GetAllPublicIds(std::list<std::string>& target,
-                                            ResourceType resourceType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?");
-    s.BindInt(0, resourceType);
-
-    target.clear();
-    while (s.Step())
-    {
-      target.push_back(s.ColumnString(0));
-    }
-  }
-
-  void DatabaseWrapperBase::GetAllPublicIds(std::list<std::string>& target,
-                                            ResourceType resourceType,
-                                            size_t since,
-                                            size_t limit)
-  {
-    if (limit == 0)
-    {
-      target.clear();
-      return;
-    }
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=? LIMIT ? OFFSET ?");
-    s.BindInt(0, resourceType);
-    s.BindInt64(1, limit);
-    s.BindInt64(2, since);
-
-    target.clear();
-    while (s.Step())
-    {
-      target.push_back(s.ColumnString(0));
-    }
-  }
-
-
-  uint64_t DatabaseWrapperBase::GetResourceCount(ResourceType resourceType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT COUNT(*) FROM Resources WHERE resourceType=?");
-    s.BindInt(0, resourceType);
-    
-    if (!s.Step())
-    {
-      return 0;
-    }
-    else
-    {
-      int64_t c = s.ColumnInt(0);
-      assert(!s.Step());
-      return c;
-    }
-  }
-
-
-  bool DatabaseWrapperBase::SelectPatientToRecycle(int64_t& internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1");
-   
-    if (!s.Step())
-    {
-      // No patient remaining or all the patients are protected
-      return false;
-    }
-    else
-    {
-      internalId = s.ColumnInt(0);
-      return true;
-    }    
-  }
-
-  bool DatabaseWrapperBase::SelectPatientToRecycle(int64_t& internalId,
-                                                   int64_t patientIdToAvoid)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT patientId FROM PatientRecyclingOrder "
-                        "WHERE patientId != ? ORDER BY seq ASC LIMIT 1");
-    s.BindInt64(0, patientIdToAvoid);
-
-    if (!s.Step())
-    {
-      // No patient remaining or all the patients are protected
-      return false;
-    }
-    else
-    {
-      internalId = s.ColumnInt(0);
-      return true;
-    }   
-  }
-
-  bool DatabaseWrapperBase::IsProtectedPatient(int64_t internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?");
-    s.BindInt64(0, internalId);
-    return !s.Step();
-  }
-
-  void DatabaseWrapperBase::SetProtectedPatient(int64_t internalId, 
-                                                bool isProtected)
-  {
-    if (isProtected)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
-      s.BindInt64(0, internalId);
-      s.Run();
-    }
-    else if (IsProtectedPatient(internalId))
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
-      s.BindInt64(0, internalId);
-      s.Run();
-    }
-    else
-    {
-      // Nothing to do: The patient is already unprotected
-    }
-  }
-
-
-
-  bool DatabaseWrapperBase::IsExistingResource(int64_t internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT * FROM Resources WHERE internalId=?");
-    s.BindInt64(0, internalId);
-    return s.Step();
-  }
-
-
-
-  void DatabaseWrapperBase::LookupIdentifier(std::list<int64_t>& target,
-                                             ResourceType level,
-                                             const DicomTag& tag,
-                                             IdentifierConstraintType type,
-                                             const std::string& value)
-  {
-    static const char* COMMON = ("SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE "
-                                 "d.id = r.internalId AND r.resourceType=? AND "
-                                 "d.tagGroup=? AND d.tagElement=? AND ");
-
-    std::auto_ptr<SQLite::Statement> s;
-
-    switch (type)
-    {
-      case IdentifierConstraintType_GreaterOrEqual:
-        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value>=?"));
-        break;
-
-      case IdentifierConstraintType_SmallerOrEqual:
-        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value<=?"));
-        break;
-
-      case IdentifierConstraintType_Wildcard:
-        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value GLOB ?"));
-        break;
-
-      case IdentifierConstraintType_Equal:
-      default:
-        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value=?"));
-        break;
-    }
-
-    assert(s.get() != NULL);
-
-    s->BindInt(0, level);
-    s->BindInt(1, tag.GetGroup());
-    s->BindInt(2, tag.GetElement());
-    s->BindString(3, value);
-
-    target.clear();
-
-    while (s->Step())
-    {
-      target.push_back(s->ColumnInt64(0));
-    }    
-  }
-}
--- a/OrthancServer/DatabaseWrapperBase.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,203 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../Core/DicomFormat/DicomMap.h"
-#include "../Core/DicomFormat/DicomTag.h"
-#include "../Core/Enumerations.h"
-#include "../Core/FileStorage/FileInfo.h"
-#include "../Core/SQLite/Connection.h"
-#include "../OrthancServer/ExportedResource.h"
-#include "../OrthancServer/ServerIndexChange.h"
-#include "ServerEnumerations.h"
-
-#include <list>
-
-
-namespace Orthanc
-{
-  /**
-   * This class is shared between the Orthanc core and the sample
-   * database plugin whose code is in
-   * "../Plugins/Samples/DatabasePlugin".
-   **/
-  class DatabaseWrapperBase
-  {
-  private:
-    SQLite::Connection&  db_;
-
-    ErrorCode GetChangesInternal(std::list<ServerIndexChange>& target,
-                                 bool& done,
-                                 SQLite::Statement& s,
-                                 uint32_t maxResults);
-
-    void GetExportedResourcesInternal(std::list<ExportedResource>& target,
-                                      bool& done,
-                                      SQLite::Statement& s,
-                                      uint32_t maxResults);
-
-  public:
-    DatabaseWrapperBase(SQLite::Connection& db) : db_(db)
-    {
-    }
-
-    void SetGlobalProperty(GlobalProperty property,
-                           const std::string& value);
-
-    bool LookupGlobalProperty(std::string& target,
-                              GlobalProperty property);
-
-    int64_t CreateResource(const std::string& publicId,
-                           ResourceType type);
-
-    bool LookupResource(int64_t& id,
-                        ResourceType& type,
-                        const std::string& publicId);
-
-    ErrorCode LookupParent(bool& found,
-                           int64_t& parentId,
-                           int64_t resourceId);
-
-    bool GetPublicId(std::string& result,
-                     int64_t resourceId);
-
-    ErrorCode GetResourceType(ResourceType& result,
-                              int64_t resourceId);
-
-    void AttachChild(int64_t parent,
-                     int64_t child);
-
-    void SetMetadata(int64_t id,
-                     MetadataType type,
-                     const std::string& value);
-
-    void DeleteMetadata(int64_t id,
-                        MetadataType type);
-
-    bool LookupMetadata(std::string& target,
-                        int64_t id,
-                        MetadataType type);
-
-    void ListAvailableMetadata(std::list<MetadataType>& target,
-                               int64_t id);
-
-    void AddAttachment(int64_t id,
-                       const FileInfo& attachment);
-
-    void DeleteAttachment(int64_t id,
-                          FileContentType attachment);
-
-    void ListAvailableAttachments(std::list<FileContentType>& target,
-                                  int64_t id);
-
-    bool LookupAttachment(FileInfo& attachment,
-                          int64_t id,
-                          FileContentType contentType);
-
-
-    void ClearMainDicomTags(int64_t id);
-
-
-    void SetMainDicomTag(int64_t id,
-                         const DicomTag& tag,
-                         const std::string& value);
-
-    void SetIdentifierTag(int64_t id,
-                          const DicomTag& tag,
-                          const std::string& value);
-
-    void GetMainDicomTags(DicomMap& map,
-                          int64_t id);
-
-    void GetChildrenPublicId(std::list<std::string>& target,
-                             int64_t id);
-
-    void GetChildrenInternalId(std::list<int64_t>& target,
-                               int64_t id);
-
-    void LogChange(int64_t internalId,
-                   const ServerIndexChange& change);
-
-    ErrorCode GetChanges(std::list<ServerIndexChange>& target,
-                         bool& done,
-                         int64_t since,
-                         uint32_t maxResults);
-
-    ErrorCode GetLastChange(std::list<ServerIndexChange>& target);
-
-    void LogExportedResource(const ExportedResource& resource);
-
-    void GetExportedResources(std::list<ExportedResource>& target,
-                              bool& done,
-                              int64_t since,
-                              uint32_t maxResults);
-    
-    void GetLastExportedResource(std::list<ExportedResource>& target);
-    
-    uint64_t GetTotalCompressedSize();
-    
-    uint64_t GetTotalUncompressedSize();
-
-    void GetAllInternalIds(std::list<int64_t>& target,
-                           ResourceType resourceType);
-
-    void GetAllPublicIds(std::list<std::string>& target,
-                         ResourceType resourceType);
-
-    void GetAllPublicIds(std::list<std::string>& target,
-                         ResourceType resourceType,
-                         size_t since,
-                         size_t limit);
-
-    uint64_t GetResourceCount(ResourceType resourceType);
-
-    bool SelectPatientToRecycle(int64_t& internalId);
-
-    bool SelectPatientToRecycle(int64_t& internalId,
-                                int64_t patientIdToAvoid);
-
-    bool IsProtectedPatient(int64_t internalId);
-
-    void SetProtectedPatient(int64_t internalId, 
-                             bool isProtected);
-
-    bool IsExistingResource(int64_t internalId);
-
-    void LookupIdentifier(std::list<int64_t>& result,
-                          ResourceType level,
-                          const DicomTag& tag,
-                          IdentifierConstraintType type,
-                          const std::string& value);
-  };
-}
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DefaultDicomImageDecoder.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IDicomImageDecoder.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
+#include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
+
+namespace Orthanc
+{
+  class DefaultDicomImageDecoder : public IDicomImageDecoder
+  {
+  public:
+    virtual ImageAccessor* Decode(const void* dicom,
+                                  size_t size,
+                                  unsigned int frame)
+    {
+      ParsedDicomFile parsed(dicom, size);
+      return DicomImageDecoder::Decode(parsed, frame);
+    }
+  };
+}
--- a/OrthancServer/DicomDirWriter.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,560 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-
-
-
-/*=========================================================================
-
-  This file is based on portions of the following project:
-
-  Program: DCMTK 3.6.0
-  Module:  http://dicom.offis.de/dcmtk.php.en
-
-Copyright (C) 1994-2011, OFFIS e.V.
-All rights reserved.
-
-This software and supporting documentation were developed by
-
-  OFFIS e.V.
-  R&D Division Health
-  Escherweg 2
-  26121 Oldenburg, Germany
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
-- Redistributions of source code must retain the above copyright
-  notice, this list of conditions and the following disclaimer.
-
-- Redistributions in binary form must reproduce the above copyright
-  notice, this list of conditions and the following disclaimer in the
-  documentation and/or other materials provided with the distribution.
-
-- Neither the name of OFFIS nor the names of its contributors may be
-  used to endorse or promote products derived from this software
-  without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-=========================================================================*/
-
-
-
-/***
-    
-    Validation:
-
-    # sudo apt-get install dicom3tools
-    # dciodvfy DICOMDIR 2>&1 | less
-    # dcentvfy DICOMDIR 2>&1 | less
-
-    http://www.dclunie.com/dicom3tools/dciodvfy.html
-
-    DICOMDIR viewer working with Wine under Linux:
-    http://www.microdicom.com/
-
- ***/
-
-
-#include "PrecompiledHeadersServer.h"
-#include "DicomDirWriter.h"
-
-#include "FromDcmtkBridge.h"
-#include "ToDcmtkBridge.h"
-
-#include "../Core/Logging.h"
-#include "../Core/OrthancException.h"
-#include "../Core/Uuid.h"
-
-#include <dcmtk/dcmdata/dcdicdir.h>
-#include <dcmtk/dcmdata/dcmetinf.h>
-#include <dcmtk/dcmdata/dcdeftag.h>
-#include <dcmtk/dcmdata/dcuid.h>
-#include <dcmtk/dcmdata/dcddirif.h>
-#include <dcmtk/dcmdata/dcvrui.h>
-#include <dcmtk/dcmdata/dcsequen.h>
-#include <dcmtk/dcmdata/dcostrmf.h>
-#include "dcmtk/dcmdata/dcvrda.h"     /* for class DcmDate */
-#include "dcmtk/dcmdata/dcvrtm.h"     /* for class DcmTime */
-
-#include <memory>
-
-namespace Orthanc
-{
-  class DicomDirWriter::PImpl
-  {
-  private:
-    std::string fileSetId_;
-    Toolbox::TemporaryFile file_;
-    std::auto_ptr<DcmDicomDir> dir_;
-
-    typedef std::pair<ResourceType, std::string>  IndexKey;
-    typedef std::map<IndexKey, DcmDirectoryRecord* >  Index;
-    Index  index_;
-
-
-    /*******************************************************************************
-     * Functions adapted from "dcmdata/libsrc/dcddirif.cc" from DCMTK 3.6.0
-     *******************************************************************************/
-
-    // print an error message to the console (stderr) that something went wrong with an attribute
-    static void printAttributeErrorMessage(const DcmTagKey &key,
-                                           const OFCondition &error,
-                                           const char *operation)
-    {
-      if (error.bad())
-      {
-        OFString str;
-        if (operation != NULL)
-        {
-          str = "cannot ";
-          str += operation;
-          str += " ";
-        }
-        LOG(ERROR) << error.text() << ": " << str << DcmTag(key).getTagName() << " " << key;
-      }
-    }
-
-    // copy element from dataset to directory record
-    static void copyElement(DcmItem& dataset,
-                            const DcmTagKey &key,
-                            DcmDirectoryRecord& record,
-                            const OFBool optional,
-                            const OFBool copyEmpty)
-    {
-      /* check whether tag exists in source dataset (if optional) */
-      if (!optional || (copyEmpty && dataset.tagExists(key)) || dataset.tagExistsWithValue(key))
-      {
-        DcmElement *delem = NULL;
-        /* get copy of element from source dataset */
-        OFCondition status = dataset.findAndGetElement(key, delem, OFFalse /*searchIntoSub*/, OFTrue /*createCopy*/);
-        if (status.good())
-        {
-          /* ... and insert it into the destination dataset (record) */
-          status = record.insert(delem, OFTrue /*replaceOld*/);
-          if (status.good())
-          {
-            DcmTag tag(key);
-            /* check for correct VR in the dataset */
-            if (delem->getVR() != tag.getEVR())
-            {
-              /* create warning message */
-              LOG(WARNING) << "DICOMDIR: possibly wrong VR: "
-                           << tag.getTagName() << " " << key << " with "
-                           << DcmVR(delem->getVR()).getVRName() << " found, expected "
-                           << tag.getVRName() << " instead";
-            }
-          } else
-            delete delem;
-        } else if (status == EC_TagNotFound)
-          status = record.insertEmptyElement(key);
-        printAttributeErrorMessage(key, status, "insert");
-      }
-    }
-
-    // copy optional string value from dataset to directory record
-    static void copyStringWithDefault(DcmItem& dataset,
-                                      const DcmTagKey &key,
-                                      DcmDirectoryRecord& record,
-                                      const char *defaultValue,
-                                      const OFBool printWarning)
-    {
-        OFCondition status;
-        if (dataset.tagExistsWithValue(key))
-        {
-          OFString stringValue;
-          /* retrieve string value from source dataset and put it into the destination dataset */
-          status = dataset.findAndGetOFStringArray(key, stringValue);
-          if (status.good())
-            status = record.putAndInsertString(key, stringValue.c_str());
-        } else {
-          if (printWarning && (defaultValue != NULL))
-          {
-            /* create warning message */
-            LOG(WARNING) << "DICOMDIR: " << DcmTag(key).getTagName() << " "
-                         << key << " missing, using alternative: " << defaultValue;
-          }
-          /* put default value */
-          status = record.putAndInsertString(key, defaultValue);
-        }
-    }
-
-    // create alternative study date if absent in dataset
-    static OFString &alternativeStudyDate(DcmItem& dataset,
-                                          OFString &result)
-    {
-      /* use another date if present */
-      if (dataset.findAndGetOFStringArray(DCM_SeriesDate, result).bad() || result.empty())
-      {
-        if (dataset.findAndGetOFStringArray(DCM_AcquisitionDate, result).bad() || result.empty())
-        {
-          if (dataset.findAndGetOFStringArray(DCM_ContentDate, result).bad() || result.empty())
-          {
-            /* use current date, "19000101" in case of error */
-            DcmDate::getCurrentDate(result);
-          }
-        }
-      }
-      return result;
-    }
-
-
-    // create alternative study time if absent in dataset
-    static OFString &alternativeStudyTime(DcmItem& dataset,
-                                          OFString &result)
-    {
-      /* use another time if present */
-      if (dataset.findAndGetOFStringArray(DCM_SeriesTime, result).bad() || result.empty())
-      {
-        if (dataset.findAndGetOFStringArray(DCM_AcquisitionTime, result).bad() || result.empty())
-        {
-          if (dataset.findAndGetOFStringArray(DCM_ContentTime, result).bad() || result.empty())
-          {
-            /* use current time, "0000" in case of error */
-            DcmTime::getCurrentTime(result);
-          }
-        }
-      }
-      return result;
-    }
-
-
-    static void copyElementType1(DcmItem& dataset,
-                                 const DcmTagKey &key,
-                                 DcmDirectoryRecord& record)
-    {
-      copyElement(dataset, key, record, OFFalse /*optional*/, OFFalse /*copyEmpty*/);
-    }
-
-    static void copyElementType1C(DcmItem& dataset,
-                                  const DcmTagKey &key,
-                                  DcmDirectoryRecord& record)
-    {
-      copyElement(dataset, key, record, OFTrue /*optional*/, OFFalse /*copyEmpty*/);
-    }
-
-    static void copyElementType2(DcmItem& dataset,
-                                 const DcmTagKey &key,
-                                 DcmDirectoryRecord& record)
-    {
-      copyElement(dataset, key, record, OFFalse /*optional*/, OFTrue /*copyEmpty*/);
-    }
-
-    /*******************************************************************************
-     * End of functions adapted from "dcmdata/libsrc/dcddirif.cc" from DCMTK 3.6.0
-     *******************************************************************************/
-
-
-    DcmDicomDir& GetDicomDir()
-    {
-      if (dir_.get() == NULL)
-      {
-        dir_.reset(new DcmDicomDir(file_.GetPath().c_str(), 
-                                   fileSetId_.c_str()));
-      }
-
-      return *dir_;
-    }
-
-
-    DcmDirectoryRecord& GetRoot()
-    {
-      return GetDicomDir().getRootRecord();
-    }
-
-
-  public:
-    PImpl() : fileSetId_("ORTHANC_MEDIA")
-    {
-    }
-
-    void FillPatient(DcmDirectoryRecord& record,
-                     DcmItem& dicom)
-    {
-      // cf. "DicomDirInterface::buildPatientRecord()"
-
-      copyElementType1C(dicom, DCM_PatientID, record);
-      copyElementType2(dicom, DCM_PatientName, record);
-    }
-
-    void FillStudy(DcmDirectoryRecord& record,
-                   DcmItem& dicom)
-    {
-      // cf. "DicomDirInterface::buildStudyRecord()"
-
-      OFString tmpString;
-      /* copy attribute values from dataset to study record */
-      copyStringWithDefault(dicom, DCM_StudyDate, record, 
-                            alternativeStudyDate(dicom, tmpString).c_str(), OFTrue /*printWarning*/);
-      copyStringWithDefault(dicom, DCM_StudyTime, record, 
-                            alternativeStudyTime(dicom, tmpString).c_str(), OFTrue /*printWarning*/);
-      copyElementType2(dicom, DCM_StudyDescription, record);
-      copyElementType1(dicom, DCM_StudyInstanceUID, record);
-      /* use type 1C instead of 1 in order to avoid unwanted overwriting */
-      copyElementType1C(dicom, DCM_StudyID, record);
-      copyElementType2(dicom, DCM_AccessionNumber, record);
-    }
-
-    void FillSeries(DcmDirectoryRecord& record,
-                    DcmItem& dicom)
-    {
-      // cf. "DicomDirInterface::buildSeriesRecord()"
-
-      /* copy attribute values from dataset to series record */
-      copyElementType1(dicom, DCM_Modality, record);
-      copyElementType1(dicom, DCM_SeriesInstanceUID, record);
-      /* use type 1C instead of 1 in order to avoid unwanted overwriting */
-      copyElementType1C(dicom, DCM_SeriesNumber, record);
-    }
-
-    void FillInstance(DcmDirectoryRecord& record,
-                      DcmItem& dicom,
-                      DcmMetaInfo& metaInfo,
-                      const char* path)
-    {
-      // cf. "DicomDirInterface::buildImageRecord()"
-
-      /* copy attribute values from dataset to image record */
-      copyElementType1(dicom, DCM_InstanceNumber, record);
-      //copyElementType1C(dicom, DCM_ImageType, record);
-      copyElementType1C(dicom, DCM_ReferencedImageSequence, record);
-
-      OFString tmp;
-
-      DcmElement* item = record.remove(DCM_ReferencedImageSequence);
-      if (item != NULL)
-      {
-        delete item;
-      }
-
-      if (record.putAndInsertString(DCM_ReferencedFileID, path).bad() ||
-          dicom.findAndGetOFStringArray(DCM_SOPClassUID, tmp).bad() ||
-          record.putAndInsertString(DCM_ReferencedSOPClassUIDInFile, tmp.c_str()).bad() ||
-          dicom.findAndGetOFStringArray(DCM_SOPInstanceUID, tmp).bad() ||
-          record.putAndInsertString(DCM_ReferencedSOPInstanceUIDInFile, tmp.c_str()).bad() ||
-          metaInfo.findAndGetOFStringArray(DCM_TransferSyntaxUID, tmp).bad() ||
-          record.putAndInsertString(DCM_ReferencedTransferSyntaxUIDInFile, tmp.c_str()).bad())
-      {
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-    }
-
-    
-
-    bool CreateResource(DcmDirectoryRecord*& target,
-                        ResourceType level,
-                        DcmFileFormat& dicom,
-                        const char* filename,
-                        const char* path)
-    {
-      DcmDataset& dataset = *dicom.getDataset();
-
-      OFCondition result;
-      OFString id;
-      E_DirRecType type;
-
-      switch (level)
-      {
-        case ResourceType_Patient:
-          result = dataset.findAndGetOFString(DCM_PatientID, id);
-          type = ERT_Patient;
-          break;
-
-        case ResourceType_Study:
-          result = dataset.findAndGetOFString(DCM_StudyInstanceUID, id);
-          type = ERT_Study;
-          break;
-
-        case ResourceType_Series:
-          result = dataset.findAndGetOFString(DCM_SeriesInstanceUID, id);
-          type = ERT_Series;
-          break;
-
-        case ResourceType_Instance:
-          result = dataset.findAndGetOFString(DCM_SOPInstanceUID, id);
-          type = ERT_Image;
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-
-      if (!result.good())
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      IndexKey key = std::make_pair(level, std::string(id.c_str()));
-      Index::iterator it = index_.find(key);
-
-      if (it != index_.end())
-      {
-        target = it->second;
-        return false; // Already existing
-      }
-
-      std::auto_ptr<DcmDirectoryRecord> record(new DcmDirectoryRecord(type, NULL, filename));
-
-      switch (level)
-      {
-        case ResourceType_Patient:
-          FillPatient(*record, dataset);
-          break;
-
-        case ResourceType_Study:
-          FillStudy(*record, dataset);
-          break;
-
-        case ResourceType_Series:
-          FillSeries(*record, dataset);
-          break;
-
-        case ResourceType_Instance:
-          FillInstance(*record, dataset, *dicom.getMetaInfo(), path);
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-
-      if (record->isAffectedBySpecificCharacterSet())
-      {
-        copyElementType1C(dataset, DCM_SpecificCharacterSet, *record);
-      }
-
-      target = record.get();
-      GetRoot().insertSub(record.release());
-      index_[key] = target;
-
-      return true;   // Newly created
-    }
-
-    void Read(std::string& s)
-    {
-      if (!GetDicomDir().write(DICOMDIR_DEFAULT_TRANSFERSYNTAX, 
-                               EET_UndefinedLength /*encodingType*/, 
-                               EGL_withoutGL /*groupLength*/).good())
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      file_.Read(s);
-    }
-
-    void SetFileSetId(const std::string& id)
-    {
-      dir_.reset(NULL);
-      fileSetId_ = id;
-    }
-  };
-
-
-  DicomDirWriter::DicomDirWriter() : pimpl_(new PImpl)
-  {
-  }
-
-  DicomDirWriter::~DicomDirWriter()
-  {
-    if (pimpl_)
-    {
-      delete pimpl_;
-    }
-  }
-
-  void DicomDirWriter::SetFileSetId(const std::string& id)
-  {
-    pimpl_->SetFileSetId(id);
-  }
-
-  void DicomDirWriter::Add(const std::string& directory,
-                           const std::string& filename,
-                           ParsedDicomFile& dicom)
-  {
-    std::string path;
-    if (directory.empty())
-    {
-      path = filename;
-    }
-    else
-    {
-      if (directory[directory.length() - 1] == '/' ||
-          directory[directory.length() - 1] == '\\')
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-
-      path = directory + '\\' + filename;
-    }
-
-    DcmFileFormat& fileFormat = *reinterpret_cast<DcmFileFormat*>(dicom.GetDcmtkObject());
-
-    DcmDirectoryRecord* instance;
-    bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, fileFormat, filename.c_str(), path.c_str());
-    if (isNewInstance)
-    {
-      DcmDirectoryRecord* series;
-      bool isNewSeries = pimpl_->CreateResource(series, ResourceType_Series, fileFormat, filename.c_str(), NULL);
-      series->insertSub(instance);
-
-      if (isNewSeries)
-      {
-        DcmDirectoryRecord* study;
-        bool isNewStudy = pimpl_->CreateResource(study, ResourceType_Study, fileFormat, filename.c_str(), NULL);
-        study->insertSub(series);
-  
-        if (isNewStudy)
-        {
-          DcmDirectoryRecord* patient;
-          pimpl_->CreateResource(patient, ResourceType_Patient, fileFormat, filename.c_str(), NULL);
-          patient->insertSub(study);
-        }
-      }
-    }
-  }
-
-  void DicomDirWriter::Encode(std::string& target)
-  {
-    pimpl_->Read(target);
-  }
-}
--- a/OrthancServer/DicomDirWriter.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ParsedDicomFile.h"
-
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class DicomDirWriter : public boost::noncopyable
-  {
-  private:
-    class PImpl;
-    PImpl* pimpl_;
-
-  public:
-    DicomDirWriter();
-
-    ~DicomDirWriter();
-
-    void SetFileSetId(const std::string& id);
-
-    void Add(const std::string& directory,
-             const std::string& filename,
-             ParsedDicomFile& dicom);
-
-    void Encode(std::string& target);
-  };
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomInstanceOrigin.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,211 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeadersServer.h"
+#include "DicomInstanceOrigin.h"
+
+#include "../Core/OrthancException.h"
+#include "../Core/SerializationToolbox.h"
+
+
+namespace Orthanc
+{
+  void DicomInstanceOrigin::Format(Json::Value& result) const
+  {
+    result = Json::objectValue;
+    result["RequestOrigin"] = EnumerationToString(origin_);
+
+    switch (origin_)
+    {
+      case RequestOrigin_Unknown:
+      {
+        // None of the methods "SetDicomProtocolOrigin()", "SetHttpOrigin()",
+        // "SetLuaOrigin()" or "SetPluginsOrigin()" was called!
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      case RequestOrigin_DicomProtocol:
+      {
+        result["RemoteIp"] = remoteIp_;
+        result["RemoteAet"] = dicomRemoteAet_;
+        result["CalledAet"] = dicomCalledAet_;
+        break;
+      }
+
+      case RequestOrigin_RestApi:
+      {
+        result["RemoteIp"] = remoteIp_;
+        result["Username"] = httpUsername_;
+        break;
+      }
+
+      case RequestOrigin_Lua:
+      case RequestOrigin_Plugins:
+      {
+        // No additional information available for these kinds of requests
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  DicomInstanceOrigin DicomInstanceOrigin::FromDicomProtocol(const char* remoteIp,
+                                                             const char* remoteAet,
+                                                             const char* calledAet)
+  {
+    DicomInstanceOrigin result(RequestOrigin_DicomProtocol);
+    result.remoteIp_ = remoteIp;
+    result.dicomRemoteAet_ = remoteAet;
+    result.dicomCalledAet_ = calledAet;
+    return result;
+  }
+
+  DicomInstanceOrigin DicomInstanceOrigin::FromRest(const RestApiCall& call)
+  {
+    DicomInstanceOrigin result(call.GetRequestOrigin());
+
+    if (result.origin_ == RequestOrigin_RestApi)
+    {
+      result.remoteIp_ = call.GetRemoteIp();
+      result.httpUsername_ = call.GetUsername();
+    }
+
+    return result;
+  }
+
+  DicomInstanceOrigin DicomInstanceOrigin::FromHttp(const char* remoteIp,
+                                                    const char* username)
+  {
+    DicomInstanceOrigin result(RequestOrigin_RestApi);
+    result.remoteIp_ = remoteIp;
+    result.httpUsername_ = username;
+    return result;
+  }
+
+  const char* DicomInstanceOrigin::GetRemoteAetC() const
+  {
+    if (origin_ == RequestOrigin_DicomProtocol)
+    {
+      return dicomRemoteAet_.c_str();
+    }
+    else
+    {
+      return "";
+    }
+  }
+
+  bool DicomInstanceOrigin::LookupRemoteAet(std::string& result) const
+  {
+    if (origin_ == RequestOrigin_DicomProtocol)
+    {
+      result = dicomRemoteAet_.c_str();
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  bool DicomInstanceOrigin::LookupRemoteIp(std::string& result) const
+  {
+    if (origin_ == RequestOrigin_DicomProtocol ||
+        origin_ == RequestOrigin_RestApi)
+    {
+      result = remoteIp_;
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  bool DicomInstanceOrigin::LookupCalledAet(std::string& result) const
+  {
+    if (origin_ == RequestOrigin_DicomProtocol)
+    {
+      result = dicomCalledAet_;
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  bool DicomInstanceOrigin::LookupHttpUsername(std::string& result) const
+  {
+    if (origin_ == RequestOrigin_RestApi)
+    {
+      result = httpUsername_;
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+
+  static const char* ORIGIN = "Origin";
+  static const char* REMOTE_IP = "RemoteIP";
+  static const char* DICOM_REMOTE_AET = "RemoteAET";
+  static const char* DICOM_CALLED_AET = "CalledAET";
+  static const char* HTTP_USERNAME = "Username";
+  
+
+  DicomInstanceOrigin::DicomInstanceOrigin(const Json::Value& serialized)
+  {
+    origin_ = StringToRequestOrigin(SerializationToolbox::ReadString(serialized, ORIGIN));
+    remoteIp_ = SerializationToolbox::ReadString(serialized, REMOTE_IP);
+    dicomRemoteAet_ = SerializationToolbox::ReadString(serialized, DICOM_REMOTE_AET);
+    dicomCalledAet_ = SerializationToolbox::ReadString(serialized, DICOM_CALLED_AET);
+    httpUsername_ = SerializationToolbox::ReadString(serialized, HTTP_USERNAME);
+  }
+  
+  
+  void DicomInstanceOrigin::Serialize(Json::Value& result) const
+  {
+    result = Json::objectValue;
+    result[ORIGIN] = EnumerationToString(origin_);
+    result[REMOTE_IP] = remoteIp_;
+    result[DICOM_REMOTE_AET] = dicomRemoteAet_;
+    result[DICOM_CALLED_AET] = dicomCalledAet_;
+    result[HTTP_USERNAME] = httpUsername_;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomInstanceOrigin.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,100 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Core/RestApi/RestApiCall.h"
+
+namespace Orthanc
+{
+  class DicomInstanceOrigin
+  {
+  private:
+    RequestOrigin origin_;
+    std::string   remoteIp_;
+    std::string   dicomRemoteAet_;
+    std::string   dicomCalledAet_;
+    std::string   httpUsername_;
+
+    DicomInstanceOrigin(RequestOrigin origin) :
+      origin_(origin)
+    {
+    }
+
+  public:
+    DicomInstanceOrigin() :
+      origin_(RequestOrigin_Unknown)
+    {
+    }
+
+    DicomInstanceOrigin(const Json::Value& serialized);
+
+    static DicomInstanceOrigin FromDicomProtocol(const char* remoteIp,
+                                                 const char* remoteAet,
+                                                 const char* calledAet);
+
+    static DicomInstanceOrigin FromRest(const RestApiCall& call);
+
+    static DicomInstanceOrigin FromHttp(const char* remoteIp,
+                                        const char* username);
+
+    static DicomInstanceOrigin FromLua()
+    {
+      return DicomInstanceOrigin(RequestOrigin_Lua);
+    }
+
+    static DicomInstanceOrigin FromPlugins()
+    {
+      return DicomInstanceOrigin(RequestOrigin_Plugins);
+    }
+
+    RequestOrigin GetRequestOrigin() const
+    {
+      return origin_;
+    }
+
+    const char* GetRemoteAetC() const; 
+
+    bool LookupRemoteAet(std::string& result) const;
+
+    bool LookupRemoteIp(std::string& result) const;
+
+    bool LookupCalledAet(std::string& result) const;
+
+    bool LookupHttpUsername(std::string& result) const;
+
+    void Format(Json::Value& result) const;
+
+    void Serialize(Json::Value& result) const;
+  };
+}
--- a/OrthancServer/DicomInstanceToStore.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/DicomInstanceToStore.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,20 +34,15 @@
 #include "PrecompiledHeadersServer.h"
 #include "DicomInstanceToStore.h"
 
-#include "FromDcmtkBridge.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/Logging.h"
 
 #include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
 
 
 namespace Orthanc
 {
-  static DcmDataset& GetDataset(ParsedDicomFile& file)
-  {
-    return *reinterpret_cast<DcmFileFormat*>(file.GetDcmtkObject())->getDataset();
-  }
-
-
   void DicomInstanceToStore::AddMetadata(ResourceType level,
                                          MetadataType metadata,
                                          const std::string& value)
@@ -69,17 +65,23 @@
     {
       if (!parsed_.HasContent())
       {
-        throw OrthancException(ErrorCode_NotImplemented);
+        if (!summary_.HasContent())
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+        else
+        {
+          parsed_.TakeOwnership(new ParsedDicomFile(summary_.GetConstContent()));
+        }                                
       }
-      else
+
+      // Serialize the parsed DICOM file
+      buffer_.Allocate();
+      if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), 
+                                               *parsed_.GetContent().GetDcmtkObject().getDataset()))
       {
-        // Serialize the parsed DICOM file
-        buffer_.Allocate();
-        if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), GetDataset(parsed_.GetContent())))
-        {
-          LOG(ERROR) << "Unable to serialize a DICOM file to a memory buffer";
-          throw OrthancException(ErrorCode_InternalError);
-        }
+        LOG(ERROR) << "Unable to serialize a DICOM file to a memory buffer";
+        throw OrthancException(ErrorCode_InternalError);
       }
     }
 
@@ -103,16 +105,18 @@
     if (!summary_.HasContent())
     {
       summary_.Allocate();
-      FromDcmtkBridge::Convert(summary_.GetContent(), GetDataset(parsed_.GetContent()));
+      FromDcmtkBridge::ExtractDicomSummary(summary_.GetContent(), 
+                                           *parsed_.GetContent().GetDcmtkObject().getDataset());
     }
     
     if (!json_.HasContent())
     {
       json_.Allocate();
-      FromDcmtkBridge::ToJson(json_.GetContent(), GetDataset(parsed_.GetContent()), 
-                              DicomToJsonFormat_Full, 
-                              DicomToJsonFlags_Default,
-                              256 /* max string length */);
+
+      std::set<DicomTag> ignoreTagLength;
+      FromDcmtkBridge::ExtractDicomAsJson(json_.GetContent(), 
+                                          *parsed_.GetContent().GetDcmtkObject().getDataset(),
+                                          ignoreTagLength);
     }
   }
 
@@ -177,97 +181,23 @@
   }
 
 
-
-  void DicomInstanceToStore::GetOriginInformation(Json::Value& result) const
+  bool DicomInstanceToStore::LookupTransferSyntax(std::string& result)
   {
-    result = Json::objectValue;
-    result["RequestOrigin"] = EnumerationToString(origin_);
-
-    switch (origin_)
-    {
-      case RequestOrigin_Unknown:
-      {
-        // None of the methods "SetDicomProtocolOrigin()", "SetHttpOrigin()",
-        // "SetLuaOrigin()" or "SetPluginsOrigin()" was called!
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-
-      case RequestOrigin_DicomProtocol:
-      {
-        result["RemoteIp"] = remoteIp_;
-        result["RemoteAet"] = dicomRemoteAet_;
-        result["CalledAet"] = dicomCalledAet_;
-        break;
-      }
-
-      case RequestOrigin_Http:
-      {
-        result["RemoteIp"] = remoteIp_;
-        result["Username"] = httpUsername_;
-        break;
-      }
-
-      case RequestOrigin_Lua:
-      case RequestOrigin_Plugins:
-      {
-        // No additional information available for these kinds of requests
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
+    ComputeMissingInformation();
 
-  void DicomInstanceToStore::SetDicomProtocolOrigin(const char* remoteIp,
-                                                    const char* remoteAet,
-                                                    const char* calledAet)
-  {
-    origin_ = RequestOrigin_DicomProtocol;
-    remoteIp_ = remoteIp;
-    dicomRemoteAet_ = remoteAet;
-    dicomCalledAet_ = calledAet;
-  }
-
-  void DicomInstanceToStore::SetRestOrigin(const RestApiCall& call)
-  {
-    origin_ = call.GetRequestOrigin();
-
-    if (origin_ == RequestOrigin_Http)
+    DicomMap header;
+    if (DicomMap::ParseDicomMetaInformation(header, GetBufferData(), GetBufferSize()))
     {
-      remoteIp_ = call.GetRemoteIp();
-      httpUsername_ = call.GetUsername();
+      const DicomValue* value = header.TestAndGetValue(DICOM_TAG_TRANSFER_SYNTAX_UID);
+      if (value != NULL &&
+          !value->IsBinary() &&
+          !value->IsNull())
+      {
+        result = Toolbox::StripSpaces(value->GetContent());
+        return true;
+      }
     }
-  }
 
-  void DicomInstanceToStore::SetHttpOrigin(const char* remoteIp,
-                                           const char* username)
-  {
-    origin_ = RequestOrigin_Http;
-    remoteIp_ = remoteIp;
-    httpUsername_ = username;
-  }
-
-  void DicomInstanceToStore::SetLuaOrigin()
-  {
-    origin_ = RequestOrigin_Lua;
-  }
-
-  void DicomInstanceToStore::SetPluginsOrigin()
-  {
-    origin_ = RequestOrigin_Plugins;
-  }
-
-  const char* DicomInstanceToStore::GetRemoteAet() const
-  {
-    if (origin_ == RequestOrigin_DicomProtocol)
-    {
-      return dicomRemoteAet_.c_str();
-    }
-    else
-    {
-      return "";
-    }
+    return false;
   }
 }
--- a/OrthancServer/DicomInstanceToStore.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/DicomInstanceToStore.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,10 +33,10 @@
 
 #pragma once
 
-#include "ParsedDicomFile.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
+#include "../Core/OrthancException.h"
+#include "DicomInstanceOrigin.h"
 #include "ServerIndex.h"
-#include "../Core/OrthancException.h"
-#include "../Core/RestApi/RestApiCall.h"
 
 namespace Orthanc
 {
@@ -138,41 +139,26 @@
       }
     };
 
-
-    SmartContainer<std::string>  buffer_;
+    DicomInstanceOrigin              origin_;
+    SmartContainer<std::string>      buffer_;
     SmartContainer<ParsedDicomFile>  parsed_;
-    SmartContainer<DicomMap>  summary_;
-    SmartContainer<Json::Value>  json_;
-
-    RequestOrigin origin_;
-    std::string remoteIp_;
-    std::string dicomRemoteAet_;
-    std::string dicomCalledAet_;
-    std::string httpUsername_;
-    ServerIndex::MetadataMap metadata_;
+    SmartContainer<DicomMap>         summary_;
+    SmartContainer<Json::Value>      json_;
+    ServerIndex::MetadataMap         metadata_;
 
     void ComputeMissingInformation();
 
   public:
-    DicomInstanceToStore() : origin_(RequestOrigin_Unknown)
+    void SetOrigin(const DicomInstanceOrigin& origin)
     {
+      origin_ = origin;
     }
-
-    void SetDicomProtocolOrigin(const char* remoteIp,
-                                const char* remoteAet,
-                                const char* calledAet);
-
-    void SetRestOrigin(const RestApiCall& call);
-
-    void SetHttpOrigin(const char* remoteIp,
-                       const char* username);
-
-    void SetLuaOrigin();
-
-    void SetPluginsOrigin();
-
-    const char* GetRemoteAet() const; 
-
+    
+    const DicomInstanceOrigin& GetOrigin() const
+    {
+      return origin_;
+    }
+    
     void SetBuffer(const std::string& dicom)
     {
       buffer_.SetConstReference(dicom);
@@ -215,6 +201,6 @@
     
     const Json::Value& GetJson();
 
-    void GetOriginInformation(Json::Value& result) const;
+    bool LookupTransferSyntax(std::string& result);
   };
 }
--- a/OrthancServer/DicomModification.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,484 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "PrecompiledHeadersServer.h"
-#include "DicomModification.h"
-
-#include "../Core/Logging.h"
-#include "../Core/OrthancException.h"
-#include "FromDcmtkBridge.h"
-
-#include <memory>   // For std::auto_ptr
-
-
-static const std::string ORTHANC_DEIDENTIFICATION_METHOD = "Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1";
-
-namespace Orthanc
-{
-  void DicomModification::RemoveInternal(const DicomTag& tag)
-  {
-    Replacements::iterator it = replacements_.find(tag);
-
-    if (it != replacements_.end())
-    {
-      delete it->second;
-      replacements_.erase(it);
-    }    
-  }
-
-
-  void DicomModification::ReplaceInternal(const DicomTag& tag,
-                                          const Json::Value& value)
-  {
-    Replacements::iterator it = replacements_.find(tag);
-
-    if (it != replacements_.end())
-    {
-      delete it->second;
-      it->second = NULL;   // In the case of an exception during the clone
-      it->second = new Json::Value(value);  // Clone
-    }
-    else
-    {
-      replacements_[tag] = new Json::Value(value);  // Clone
-    }
-  }
-
-
-  void DicomModification::ClearReplacements()
-  {
-    for (Replacements::iterator it = replacements_.begin();
-         it != replacements_.end(); ++it)
-    {
-      delete it->second;
-    }
-
-    replacements_.clear();
-  }
-
-
-  void DicomModification::MarkNotOrthancAnonymization()
-  {
-    Replacements::iterator it = replacements_.find(DICOM_TAG_DEIDENTIFICATION_METHOD);
-
-    if (it != replacements_.end() &&
-        it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD)
-    {
-      delete it->second;
-      replacements_.erase(it);
-    }
-  }
-
-
-  void DicomModification::MapDicomIdentifier(ParsedDicomFile& dicom,
-                                             ResourceType level)
-  {
-    std::auto_ptr<DicomTag> tag;
-
-    switch (level)
-    {
-      case ResourceType_Study:
-        tag.reset(new DicomTag(DICOM_TAG_STUDY_INSTANCE_UID));
-        break;
-
-      case ResourceType_Series:
-        tag.reset(new DicomTag(DICOM_TAG_SERIES_INSTANCE_UID));
-        break;
-
-      case ResourceType_Instance:
-        tag.reset(new DicomTag(DICOM_TAG_SOP_INSTANCE_UID));
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    std::string original;
-    if (!dicom.GetTagValue(original, *tag))
-    {
-      original = "";
-    }
-
-    std::string mapped;
-
-    UidMap::const_iterator previous = uidMap_.find(std::make_pair(level, original));
-    if (previous == uidMap_.end())
-    {
-      mapped = FromDcmtkBridge::GenerateUniqueIdentifier(level);
-      uidMap_.insert(std::make_pair(std::make_pair(level, original), mapped));
-    }
-    else
-    {
-      mapped = previous->second;
-    }    
-
-    dicom.Replace(*tag, mapped);
-  }
-  
-  DicomModification::DicomModification()
-  {
-    removePrivateTags_ = false;
-    level_ = ResourceType_Instance;
-    allowManualIdentifiers_ = true;
-  }
-
-  DicomModification::~DicomModification()
-  {
-    ClearReplacements();
-  }
-
-  void DicomModification::Keep(const DicomTag& tag)
-  {
-    removals_.erase(tag);
-    RemoveInternal(tag);
-
-    if (FromDcmtkBridge::IsPrivateTag(tag))
-    {
-      privateTagsToKeep_.insert(tag);
-    }
-
-    MarkNotOrthancAnonymization();
-  }
-
-  void DicomModification::Remove(const DicomTag& tag)
-  {
-    removals_.insert(tag);
-    RemoveInternal(tag);
-    privateTagsToKeep_.erase(tag);
-
-    MarkNotOrthancAnonymization();
-  }
-
-  bool DicomModification::IsRemoved(const DicomTag& tag) const
-  {
-    return removals_.find(tag) != removals_.end();
-  }
-
-  void DicomModification::Replace(const DicomTag& tag,
-                                  const Json::Value& value,
-                                  bool safeForAnonymization)
-  {
-    removals_.erase(tag);
-    privateTagsToKeep_.erase(tag);
-    ReplaceInternal(tag, value);
-
-    if (!safeForAnonymization)
-    {
-      MarkNotOrthancAnonymization();
-    }
-  }
-
-
-  bool DicomModification::IsReplaced(const DicomTag& tag) const
-  {
-    return replacements_.find(tag) != replacements_.end();
-  }
-
-  const Json::Value& DicomModification::GetReplacement(const DicomTag& tag) const
-  {
-    Replacements::const_iterator it = replacements_.find(tag);
-
-    if (it == replacements_.end())
-    {
-      throw OrthancException(ErrorCode_InexistentItem);
-    }
-    else
-    {
-      return *it->second;
-    } 
-  }
-
-
-  std::string DicomModification::GetReplacementAsString(const DicomTag& tag) const
-  {
-    const Json::Value& json = GetReplacement(tag);
-
-    if (json.type() != Json::stringValue)
-    {
-      throw OrthancException(ErrorCode_BadParameterType);
-    }
-    else
-    {
-      return json.asString();
-    }    
-  }
-
-
-  void DicomModification::SetRemovePrivateTags(bool removed)
-  {
-    removePrivateTags_ = removed;
-
-    if (!removed)
-    {
-      MarkNotOrthancAnonymization();
-    }
-  }
-
-  void DicomModification::SetLevel(ResourceType level)
-  {
-    uidMap_.clear();
-    level_ = level;
-
-    if (level != ResourceType_Patient)
-    {
-      MarkNotOrthancAnonymization();
-    }
-  }
-
-  void DicomModification::SetupAnonymization()
-  {
-    removals_.clear();
-    ClearReplacements();
-    removePrivateTags_ = true;
-    level_ = ResourceType_Patient;
-    uidMap_.clear();
-    privateTagsToKeep_.clear();
-
-    // This is Table E.1-1 from PS 3.15-2008 - DICOM Part 15: Security and System Management Profiles
-    removals_.insert(DicomTag(0x0008, 0x0014));  // Instance Creator UID
-    //removals_.insert(DicomTag(0x0008, 0x0018));  // SOP Instance UID => set in Apply()
-    removals_.insert(DicomTag(0x0008, 0x0050));  // Accession Number
-    removals_.insert(DicomTag(0x0008, 0x0080));  // Institution Name
-    removals_.insert(DicomTag(0x0008, 0x0081));  // Institution Address
-    removals_.insert(DicomTag(0x0008, 0x0090));  // Referring Physician's Name 
-    removals_.insert(DicomTag(0x0008, 0x0092));  // Referring Physician's Address 
-    removals_.insert(DicomTag(0x0008, 0x0094));  // Referring Physician's Telephone Numbers 
-    removals_.insert(DicomTag(0x0008, 0x1010));  // Station Name 
-    removals_.insert(DicomTag(0x0008, 0x1030));  // Study Description 
-    removals_.insert(DicomTag(0x0008, 0x103e));  // Series Description 
-    removals_.insert(DicomTag(0x0008, 0x1040));  // Institutional Department Name 
-    removals_.insert(DicomTag(0x0008, 0x1048));  // Physician(s) of Record 
-    removals_.insert(DicomTag(0x0008, 0x1050));  // Performing Physicians' Name 
-    removals_.insert(DicomTag(0x0008, 0x1060));  // Name of Physician(s) Reading Study 
-    removals_.insert(DicomTag(0x0008, 0x1070));  // Operators' Name 
-    removals_.insert(DicomTag(0x0008, 0x1080));  // Admitting Diagnoses Description 
-    removals_.insert(DicomTag(0x0008, 0x1155));  // Referenced SOP Instance UID 
-    removals_.insert(DicomTag(0x0008, 0x2111));  // Derivation Description 
-    //removals_.insert(DicomTag(0x0010, 0x0010));  // Patient's Name => cf. below (*)
-    //removals_.insert(DicomTag(0x0010, 0x0020));  // Patient ID => cf. below (*)
-    removals_.insert(DicomTag(0x0010, 0x0030));  // Patient's Birth Date 
-    removals_.insert(DicomTag(0x0010, 0x0032));  // Patient's Birth Time 
-    removals_.insert(DicomTag(0x0010, 0x0040));  // Patient's Sex 
-    removals_.insert(DicomTag(0x0010, 0x1000));  // Other Patient Ids 
-    removals_.insert(DicomTag(0x0010, 0x1001));  // Other Patient Names 
-    removals_.insert(DicomTag(0x0010, 0x1010));  // Patient's Age 
-    removals_.insert(DicomTag(0x0010, 0x1020));  // Patient's Size 
-    removals_.insert(DicomTag(0x0010, 0x1030));  // Patient's Weight 
-    removals_.insert(DicomTag(0x0010, 0x1090));  // Medical Record Locator 
-    removals_.insert(DicomTag(0x0010, 0x2160));  // Ethnic Group 
-    removals_.insert(DicomTag(0x0010, 0x2180));  // Occupation 
-    removals_.insert(DicomTag(0x0010, 0x21b0));  // Additional Patient's History 
-    removals_.insert(DicomTag(0x0010, 0x4000));  // Patient Comments 
-    removals_.insert(DicomTag(0x0018, 0x1000));  // Device Serial Number 
-    removals_.insert(DicomTag(0x0018, 0x1030));  // Protocol Name 
-    //removals_.insert(DicomTag(0x0020, 0x000d));  // Study Instance UID => set in Apply()
-    //removals_.insert(DicomTag(0x0020, 0x000e));  // Series Instance UID => set in Apply()
-    removals_.insert(DicomTag(0x0020, 0x0010));  // Study ID 
-    removals_.insert(DicomTag(0x0020, 0x0052));  // Frame of Reference UID 
-    removals_.insert(DicomTag(0x0020, 0x0200));  // Synchronization Frame of Reference UID 
-    removals_.insert(DicomTag(0x0020, 0x4000));  // Image Comments 
-    removals_.insert(DicomTag(0x0040, 0x0275));  // Request Attributes Sequence 
-    removals_.insert(DicomTag(0x0040, 0xa124));  // UID
-    removals_.insert(DicomTag(0x0040, 0xa730));  // Content Sequence 
-    removals_.insert(DicomTag(0x0088, 0x0140));  // Storage Media File-set UID 
-    removals_.insert(DicomTag(0x3006, 0x0024));  // Referenced Frame of Reference UID 
-    removals_.insert(DicomTag(0x3006, 0x00c2));  // Related Frame of Reference UID 
-
-    // Some more removals (from the experience of DICOM files at the CHU of Liege)
-    removals_.insert(DicomTag(0x0010, 0x1040));  // Patient's Address
-    removals_.insert(DicomTag(0x0032, 0x1032));  // Requesting Physician
-    removals_.insert(DicomTag(0x0010, 0x2154));  // PatientTelephoneNumbers
-    removals_.insert(DicomTag(0x0010, 0x2000));  // Medical Alerts
-
-    // Set the DeidentificationMethod tag
-    ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD);
-
-    // Set the PatientIdentityRemoved tag
-    ReplaceInternal(DicomTag(0x0012, 0x0062), "YES");
-
-    // (*) Choose a random patient name and ID
-    std::string patientId = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient);
-    ReplaceInternal(DICOM_TAG_PATIENT_ID, patientId);
-    ReplaceInternal(DICOM_TAG_PATIENT_NAME, patientId);
-  }
-
-  void DicomModification::Apply(ParsedDicomFile& toModify)
-  {
-    // Check the request
-    assert(ResourceType_Patient + 1 == ResourceType_Study &&
-           ResourceType_Study + 1 == ResourceType_Series &&
-           ResourceType_Series + 1 == ResourceType_Instance);
-
-    if (IsRemoved(DICOM_TAG_PATIENT_ID) ||
-        IsRemoved(DICOM_TAG_STUDY_INSTANCE_UID) ||
-        IsRemoved(DICOM_TAG_SERIES_INSTANCE_UID) ||
-        IsRemoved(DICOM_TAG_SOP_INSTANCE_UID))
-    {
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-    
-
-    // Sanity checks at the patient level
-    if (level_ == ResourceType_Patient && !IsReplaced(DICOM_TAG_PATIENT_ID))
-    {
-      LOG(ERROR) << "When modifying a patient, her PatientID is required to be modified";
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-
-    if (!allowManualIdentifiers_)
-    {
-      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
-      {
-        LOG(ERROR) << "When modifying a patient, the StudyInstanceUID cannot be manually modified";
-        throw OrthancException(ErrorCode_BadRequest);
-      }
-
-      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
-      {
-        LOG(ERROR) << "When modifying a patient, the SeriesInstanceUID cannot be manually modified";
-        throw OrthancException(ErrorCode_BadRequest);
-      }
-
-      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
-      {
-        LOG(ERROR) << "When modifying a patient, the SopInstanceUID cannot be manually modified";
-        throw OrthancException(ErrorCode_BadRequest);
-      }
-    }
-
-
-    // Sanity checks at the study level
-    if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_PATIENT_ID))
-    {
-      LOG(ERROR) << "When modifying a study, the parent PatientID cannot be manually modified";
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-
-    if (!allowManualIdentifiers_)
-    {
-      if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
-      {
-        LOG(ERROR) << "When modifying a study, the SeriesInstanceUID cannot be manually modified";
-        throw OrthancException(ErrorCode_BadRequest);
-      }
-
-      if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
-      {
-        LOG(ERROR) << "When modifying a study, the SopInstanceUID cannot be manually modified";
-        throw OrthancException(ErrorCode_BadRequest);
-      }
-    }
-
-
-    // Sanity checks at the series level
-    if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_PATIENT_ID))
-    {
-      LOG(ERROR) << "When modifying a series, the parent PatientID cannot be manually modified";
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-
-    if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
-    {
-      LOG(ERROR) << "When modifying a series, the parent StudyInstanceUID cannot be manually modified";
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-
-    if (!allowManualIdentifiers_)
-    {
-      if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
-      {
-        LOG(ERROR) << "When modifying a series, the SopInstanceUID cannot be manually modified";
-        throw OrthancException(ErrorCode_BadRequest);
-      }
-    }
-
-
-    // Sanity checks at the instance level
-    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_PATIENT_ID))
-    {
-      LOG(ERROR) << "When modifying an instance, the parent PatientID cannot be manually modified";
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-
-    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
-    {
-      LOG(ERROR) << "When modifying an instance, the parent StudyInstanceUID cannot be manually modified";
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-
-    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
-    {
-      LOG(ERROR) << "When modifying an instance, the parent SeriesInstanceUID cannot be manually modified";
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-
-
-    // (1) Remove the private tags, if need be
-    if (removePrivateTags_)
-    {
-      toModify.RemovePrivateTags(privateTagsToKeep_);
-    }
-
-    // (2) Remove the tags specified by the user
-    for (SetOfTags::const_iterator it = removals_.begin(); 
-         it != removals_.end(); ++it)
-    {
-      toModify.Remove(*it);
-    }
-
-    // (3) Replace the tags
-    for (Replacements::const_iterator it = replacements_.begin(); 
-         it != replacements_.end(); ++it)
-    {
-      toModify.Replace(it->first, *it->second, DicomReplaceMode_InsertIfAbsent);
-    }
-
-    // (4) Update the DICOM identifiers
-    if (level_ <= ResourceType_Study &&
-        !IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
-    {
-      MapDicomIdentifier(toModify, ResourceType_Study);
-    }
-
-    if (level_ <= ResourceType_Series &&
-        !IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
-    {
-      MapDicomIdentifier(toModify, ResourceType_Series);
-    }
-
-    if (level_ <= ResourceType_Instance &&  // Always true
-        !IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
-    {
-      MapDicomIdentifier(toModify, ResourceType_Instance);
-    }
-  }
-}
--- a/OrthancServer/DicomModification.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ParsedDicomFile.h"
-
-namespace Orthanc
-{
-  class DicomModification : public boost::noncopyable
-  {
-    /**
-     * Process:
-     * (1) Remove private tags
-     * (2) Remove tags specified by the user
-     * (3) Replace tags
-     **/
-
-  private:
-    typedef std::set<DicomTag> SetOfTags;
-    typedef std::map<DicomTag, Json::Value*> Replacements;
-    typedef std::map< std::pair<ResourceType, std::string>, std::string>  UidMap;
-
-    SetOfTags removals_;
-    Replacements replacements_;
-    bool removePrivateTags_;
-    ResourceType level_;
-    UidMap uidMap_;
-    SetOfTags privateTagsToKeep_;
-    bool allowManualIdentifiers_;
-
-    void MapDicomIdentifier(ParsedDicomFile& dicom,
-                            ResourceType level);
-
-    void MarkNotOrthancAnonymization();
-
-    void ClearReplacements();
-
-    void RemoveInternal(const DicomTag& tag);
-
-    void ReplaceInternal(const DicomTag& tag,
-                         const Json::Value& value);
-
-  public:
-    DicomModification();
-
-    ~DicomModification();
-
-    void Keep(const DicomTag& tag);
-
-    void Remove(const DicomTag& tag);
-
-    bool IsRemoved(const DicomTag& tag) const;
-
-    void Replace(const DicomTag& tag,
-                 const Json::Value& value,   // Encoded using UTF-8
-                 bool safeForAnonymization = false);
-
-    bool IsReplaced(const DicomTag& tag) const;
-
-    const Json::Value& GetReplacement(const DicomTag& tag) const;
-
-    std::string GetReplacementAsString(const DicomTag& tag) const;
-
-    void SetRemovePrivateTags(bool removed);
-
-    bool ArePrivateTagsRemoved() const
-    {
-      return removePrivateTags_;
-    }
-
-    void SetLevel(ResourceType level);
-
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    void SetupAnonymization();
-
-    void Apply(ParsedDicomFile& toModify);
-
-    void SetAllowManualIdentifiers(bool check)
-    {
-      allowManualIdentifiers_ = check;
-    }
-
-    bool AreAllowManualIdentifiers() const
-    {
-      return allowManualIdentifiers_;
-    }
-  };
-}
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "DicomFindAnswers.h"
-
-#include "../FromDcmtkBridge.h"
-
-namespace Orthanc
-{
-  void DicomFindAnswers::Clear()
-  {
-    for (size_t i = 0; i < items_.size(); i++)
-    {
-      delete items_[i];
-    }
-  }
-
-  void DicomFindAnswers::Reserve(size_t size)
-  {
-    if (size > items_.size())
-    {
-      items_.reserve(size);
-    }
-  }
-
-  void DicomFindAnswers::ToJson(Json::Value& target,
-                                bool simplify) const
-  {
-    target = Json::arrayValue;
-
-    for (size_t i = 0; i < GetSize(); i++)
-    {
-      Json::Value answer(Json::objectValue);
-      FromDcmtkBridge::ToJson(answer, GetAnswer(i), simplify);
-      target.append(answer);
-    }
-  }
-}
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../../Core/DicomFormat/DicomMap.h"
-
-#include <vector>
-#include <json/json.h>
-
-namespace Orthanc
-{
-  class DicomFindAnswers
-  {
-  private:
-    std::vector<DicomMap*> items_;
-
-  public:
-    ~DicomFindAnswers()
-    {
-      Clear();
-    }
-
-    void Clear();
-
-    void Reserve(size_t index);
-
-    void Add(const DicomMap& map)
-    {
-      items_.push_back(map.Clone());
-    }
-
-    size_t GetSize() const
-    {
-      return items_.size();
-    }
-
-    const DicomMap& GetAnswer(size_t index) const
-    {
-      return *items_.at(index);
-    }
-
-    void ToJson(Json::Value& target,
-                bool simplify) const;
-  };
-}
--- a/OrthancServer/DicomProtocol/DicomServer.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,325 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "DicomServer.h"
-
-#include "../../Core/Logging.h"
-#include "../../Core/OrthancException.h"
-#include "../../Core/Toolbox.h"
-#include "../../Core/Uuid.h"
-#include "../Internals/CommandDispatcher.h"
-#include "../OrthancInitialization.h"
-#include "EmbeddedResources.h"
-#include "../../Core/MultiThreading/RunnableWorkersPool.h"
-
-#include <boost/thread.hpp>
-
-#if defined(__linux)
-#include <cstdlib>
-#endif
-
-
-namespace Orthanc
-{
-  struct DicomServer::PImpl
-  {
-    boost::thread  thread_;
-    T_ASC_Network *network_;
-    std::auto_ptr<RunnableWorkersPool>  workers_;
-  };
-
-
-  void DicomServer::ServerThread(DicomServer* server)
-  {
-    LOG(INFO) << "DICOM server started";
-
-    while (server->continue_)
-    {
-      /* receive an association and acknowledge or reject it. If the association was */
-      /* acknowledged, offer corresponding services and invoke one or more if required. */
-      std::auto_ptr<Internals::CommandDispatcher> dispatcher(Internals::AcceptAssociation(*server, server->pimpl_->network_));
-
-      try
-      {
-        if (dispatcher.get() != NULL)
-        {
-          server->pimpl_->workers_->Add(dispatcher.release());
-        }
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Exception in the DICOM server thread: " << e.What();
-      }
-    }
-
-    LOG(INFO) << "DICOM server stopping";
-  }
-
-
-  DicomServer::DicomServer() : 
-    pimpl_(new PImpl),
-    aet_("ANY-SCP")
-  {
-    port_ = 104;
-    findRequestHandlerFactory_ = NULL;
-    moveRequestHandlerFactory_ = NULL;
-    storeRequestHandlerFactory_ = NULL;
-    applicationEntityFilter_ = NULL;
-    checkCalledAet_ = true;
-    clientTimeout_ = 30;
-    continue_ = false;
-  }
-
-  DicomServer::~DicomServer()
-  {
-    if (continue_)
-    {
-      LOG(ERROR) << "INTERNAL ERROR: DicomServer::Stop() should be invoked manually to avoid mess in the destruction order!";
-      Stop();
-    }
-  }
-
-  void DicomServer::SetPortNumber(uint16_t port)
-  {
-    Stop();
-    port_ = port;
-  }
-
-  uint16_t DicomServer::GetPortNumber() const
-  {
-    return port_;
-  }
-
-  void DicomServer::SetClientTimeout(uint32_t timeout)
-  {
-    Stop();
-    clientTimeout_ = timeout;
-  }
-
-  uint32_t DicomServer::GetClientTimeout() const
-  {
-    return clientTimeout_;
-  }
-
-
-  void DicomServer::SetCalledApplicationEntityTitleCheck(bool check)
-  {
-    Stop();
-    checkCalledAet_ = check;
-  }
-
-  bool DicomServer::HasCalledApplicationEntityTitleCheck() const
-  {
-    return checkCalledAet_;
-  }
-
-  void DicomServer::SetApplicationEntityTitle(const std::string& aet)
-  {
-    if (aet.size() == 0)
-    {
-      throw OrthancException(ErrorCode_BadApplicationEntityTitle);
-    }
-
-    if (aet.size() > 16)
-    {
-      throw OrthancException(ErrorCode_BadApplicationEntityTitle);
-    }
-
-    for (size_t i = 0; i < aet.size(); i++)
-    {
-      if (!(aet[i] == '-' ||
-            aet[i] == '_' ||
-            isdigit(aet[i]) ||
-            (aet[i] >= 'A' && aet[i] <= 'Z')))
-      {
-        LOG(WARNING) << "For best interoperability, only upper case, alphanumeric characters should be present in AET: \"" << aet << "\"";
-        break;
-      }
-    }
-
-    Stop();
-    aet_ = aet;
-  }
-
-  const std::string& DicomServer::GetApplicationEntityTitle() const
-  {
-    return aet_;
-  }
-
-  void DicomServer::SetFindRequestHandlerFactory(IFindRequestHandlerFactory& factory)
-  {
-    Stop();
-    findRequestHandlerFactory_ = &factory;
-  }
-
-  bool DicomServer::HasFindRequestHandlerFactory() const
-  {
-    return (findRequestHandlerFactory_ != NULL);
-  }
-
-  IFindRequestHandlerFactory& DicomServer::GetFindRequestHandlerFactory() const
-  {
-    if (HasFindRequestHandlerFactory())
-    {
-      return *findRequestHandlerFactory_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_NoCFindHandler);
-    }
-  }
-
-  void DicomServer::SetMoveRequestHandlerFactory(IMoveRequestHandlerFactory& factory)
-  {
-    Stop();
-    moveRequestHandlerFactory_ = &factory;
-  }
-
-  bool DicomServer::HasMoveRequestHandlerFactory() const
-  {
-    return (moveRequestHandlerFactory_ != NULL);
-  }
-
-  IMoveRequestHandlerFactory& DicomServer::GetMoveRequestHandlerFactory() const
-  {
-    if (HasMoveRequestHandlerFactory())
-    {
-      return *moveRequestHandlerFactory_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_NoCMoveHandler);
-    }
-  }
-
-  void DicomServer::SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& factory)
-  {
-    Stop();
-    storeRequestHandlerFactory_ = &factory;
-  }
-
-  bool DicomServer::HasStoreRequestHandlerFactory() const
-  {
-    return (storeRequestHandlerFactory_ != NULL);
-  }
-
-  IStoreRequestHandlerFactory& DicomServer::GetStoreRequestHandlerFactory() const
-  {
-    if (HasStoreRequestHandlerFactory())
-    {
-      return *storeRequestHandlerFactory_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_NoCStoreHandler);
-    }
-  }
-
-  void DicomServer::SetApplicationEntityFilter(IApplicationEntityFilter& factory)
-  {
-    Stop();
-    applicationEntityFilter_ = &factory;
-  }
-
-  bool DicomServer::HasApplicationEntityFilter() const
-  {
-    return (applicationEntityFilter_ != NULL);
-  }
-
-  IApplicationEntityFilter& DicomServer::GetApplicationEntityFilter() const
-  {
-    if (HasApplicationEntityFilter())
-    {
-      return *applicationEntityFilter_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_NoApplicationEntityFilter);
-    }
-  }
-
-  void DicomServer::Start()
-  {
-    Stop();
-
-    /* initialize network, i.e. create an instance of T_ASC_Network*. */
-    OFCondition cond = ASC_initializeNetwork
-      (NET_ACCEPTOR, OFstatic_cast(int, port_), /*opt_acse_timeout*/ 30, &pimpl_->network_);
-    if (cond.bad())
-    {
-      LOG(ERROR) << "cannot create network: " << cond.text();
-      throw OrthancException(ErrorCode_DicomPortInUse);
-    }
-
-    continue_ = true;
-    pimpl_->workers_.reset(new RunnableWorkersPool(4));   // Use 4 workers - TODO as a parameter?
-    pimpl_->thread_ = boost::thread(ServerThread, this);
-  }
-
-
-  void DicomServer::Stop()
-  {
-    if (continue_)
-    {
-      continue_ = false;
-
-      if (pimpl_->thread_.joinable())
-      {
-        pimpl_->thread_.join();
-      }
-
-      pimpl_->workers_.reset(NULL);
-
-      /* drop the network, i.e. free memory of T_ASC_Network* structure. This call */
-      /* is the counterpart of ASC_initializeNetwork(...) which was called above. */
-      OFCondition cond = ASC_dropNetwork(&pimpl_->network_);
-      if (cond.bad())
-      {
-        LOG(ERROR) << "Error while dropping the network: " << cond.text();
-      }
-    }
-  }
-
-
-  bool DicomServer::IsMyAETitle(const std::string& aet) const
-  {
-    if (!HasCalledApplicationEntityTitleCheck())
-    {
-      // OK, no check on the AET.
-      return true;
-    }
-
-    return Configuration::IsSameAETitle(aet, GetApplicationEntityTitle());
-  }
-
-}
--- a/OrthancServer/DicomProtocol/DicomServer.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IFindRequestHandlerFactory.h"
-#include "IMoveRequestHandlerFactory.h"
-#include "IStoreRequestHandlerFactory.h"
-#include "IApplicationEntityFilter.h"
-
-#include <boost/shared_ptr.hpp>
-#include <boost/noncopyable.hpp>
-
-
-namespace Orthanc
-{
-  class DicomServer : public boost::noncopyable
-  {
-  private:
-    struct PImpl;
-    boost::shared_ptr<PImpl> pimpl_;
-
-    bool checkCalledAet_;
-    std::string aet_;
-    uint16_t port_;
-    bool continue_;
-    bool started_;
-    uint32_t clientTimeout_;
-    IFindRequestHandlerFactory* findRequestHandlerFactory_;
-    IMoveRequestHandlerFactory* moveRequestHandlerFactory_;
-    IStoreRequestHandlerFactory* storeRequestHandlerFactory_;
-    IApplicationEntityFilter* applicationEntityFilter_;
-
-    static void ServerThread(DicomServer* server);
-
-  public:
-    DicomServer();
-
-    ~DicomServer();
-
-    void SetPortNumber(uint16_t port);
-    uint16_t GetPortNumber() const;
-
-    void SetClientTimeout(uint32_t timeout);
-    uint32_t GetClientTimeout() const;
-
-    void SetCalledApplicationEntityTitleCheck(bool check);
-    bool HasCalledApplicationEntityTitleCheck() const;
-
-    void SetApplicationEntityTitle(const std::string& aet);
-    const std::string& GetApplicationEntityTitle() const;
-
-    void SetFindRequestHandlerFactory(IFindRequestHandlerFactory& handler);
-    bool HasFindRequestHandlerFactory() const;
-    IFindRequestHandlerFactory& GetFindRequestHandlerFactory() const;
-
-    void SetMoveRequestHandlerFactory(IMoveRequestHandlerFactory& handler);
-    bool HasMoveRequestHandlerFactory() const;
-    IMoveRequestHandlerFactory& GetMoveRequestHandlerFactory() const;
-
-    void SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& handler);
-    bool HasStoreRequestHandlerFactory() const;
-    IStoreRequestHandlerFactory& GetStoreRequestHandlerFactory() const;
-
-    void SetApplicationEntityFilter(IApplicationEntityFilter& handler);
-    bool HasApplicationEntityFilter() const;
-    IApplicationEntityFilter& GetApplicationEntityFilter() const;
-
-    void Start();
-  
-    void Stop();
-
-    bool IsMyAETitle(const std::string& aet) const;
-  };
-
-}
--- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1104 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-
-/*=========================================================================
-
-  This file is based on portions of the following project:
-
-  Program: DCMTK 3.6.0
-  Module:  http://dicom.offis.de/dcmtk.php.en
-
-Copyright (C) 1994-2011, OFFIS e.V.
-All rights reserved.
-
-This software and supporting documentation were developed by
-
-  OFFIS e.V.
-  R&D Division Health
-  Escherweg 2
-  26121 Oldenburg, Germany
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
-- Redistributions of source code must retain the above copyright
-  notice, this list of conditions and the following disclaimer.
-
-- Redistributions in binary form must reproduce the above copyright
-  notice, this list of conditions and the following disclaimer in the
-  documentation and/or other materials provided with the distribution.
-
-- Neither the name of OFFIS nor the names of its contributors may be
-  used to endorse or promote products derived from this software
-  without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-=========================================================================*/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "DicomUserConnection.h"
-
-#include "../../Core/DicomFormat/DicomArray.h"
-#include "../../Core/Logging.h"
-#include "../../Core/OrthancException.h"
-#include "../FromDcmtkBridge.h"
-#include "../ToDcmtkBridge.h"
-
-#include <dcmtk/dcmdata/dcistrmb.h>
-#include <dcmtk/dcmdata/dcistrmf.h>
-#include <dcmtk/dcmdata/dcfilefo.h>
-#include <dcmtk/dcmdata/dcmetinf.h>
-#include <dcmtk/dcmnet/diutil.h>
-
-#include <set>
-
-
-#ifdef _WIN32
-/**
- * "The maximum length, in bytes, of the string returned in the buffer 
- * pointed to by the name parameter is dependent on the namespace provider,
- * but this string must be 256 bytes or less.
- * http://msdn.microsoft.com/en-us/library/windows/desktop/ms738527(v=vs.85).aspx
- **/
-#define HOST_NAME_MAX 256
-#endif 
-
-
-#if !defined(HOST_NAME_MAX) && defined(_POSIX_HOST_NAME_MAX)
-/**
- * TO IMPROVE: "_POSIX_HOST_NAME_MAX is only the minimum value that
- * HOST_NAME_MAX can ever have [...] Therefore you cannot allocate an
- * array of size _POSIX_HOST_NAME_MAX, invoke gethostname() and expect
- * that the result will fit."
- * http://lists.gnu.org/archive/html/bug-gnulib/2009-08/msg00128.html
- **/
-#define HOST_NAME_MAX _POSIX_HOST_NAME_MAX
-#endif
-
-
-static const char* DEFAULT_PREFERRED_TRANSFER_SYNTAX = UID_LittleEndianImplicitTransferSyntax;
-
-/**
- * "If we have more than 64 storage SOP classes, tools such as
- * storescu will fail because they attempt to negotiate two
- * presentation contexts for each SOP class, and there is a total
- * limit of 128 contexts for one association."
- **/
-static const unsigned int MAXIMUM_STORAGE_SOP_CLASSES = 64;
-
-
-namespace Orthanc
-{
-  struct DicomUserConnection::PImpl
-  {
-    // Connection state
-    uint32_t dimseTimeout_;
-    uint32_t acseTimeout_;
-    T_ASC_Network* net_;
-    T_ASC_Parameters* params_;
-    T_ASC_Association* assoc_;
-
-    bool IsOpen() const
-    {
-      return assoc_ != NULL;
-    }
-
-    void CheckIsOpen() const;
-
-    void Store(DcmInputStream& is, DicomUserConnection& connection);
-  };
-
-
-  static void Check(const OFCondition& cond)
-  {
-    if (cond.bad())
-    {
-      LOG(ERROR) << "DicomUserConnection: " << std::string(cond.text());
-       throw OrthancException(ErrorCode_NetworkProtocol);
-    }
-  }
-
-  void DicomUserConnection::PImpl::CheckIsOpen() const
-  {
-    if (!IsOpen())
-    {
-      LOG(ERROR) << "DicomUserConnection: First open the connection";
-      throw OrthancException(ErrorCode_NetworkProtocol);
-    }
-  }
-
-
-  void DicomUserConnection::CheckIsOpen() const
-  {
-    pimpl_->CheckIsOpen();
-  }
-
-
-  static void RegisterStorageSOPClass(T_ASC_Parameters* params,
-                                      unsigned int& presentationContextId,
-                                      const std::string& sopClass,
-                                      const char* asPreferred[],
-                                      std::vector<const char*>& asFallback)
-  {
-    Check(ASC_addPresentationContext(params, presentationContextId, 
-                                     sopClass.c_str(), asPreferred, 1));
-    presentationContextId += 2;
-
-    if (asFallback.size() > 0)
-    {
-      Check(ASC_addPresentationContext(params, presentationContextId, 
-                                       sopClass.c_str(), &asFallback[0], asFallback.size()));
-      presentationContextId += 2;
-    }
-  }
-  
-    
-  void DicomUserConnection::SetupPresentationContexts(const std::string& preferredTransferSyntax)
-  {
-    // Flatten an array with the preferred transfer syntax
-    const char* asPreferred[1] = { preferredTransferSyntax.c_str() };
-
-    // Setup the fallback transfer syntaxes
-    std::set<std::string> fallbackSyntaxes;
-    fallbackSyntaxes.insert(UID_LittleEndianExplicitTransferSyntax);
-    fallbackSyntaxes.insert(UID_BigEndianExplicitTransferSyntax);
-    fallbackSyntaxes.insert(UID_LittleEndianImplicitTransferSyntax);
-    fallbackSyntaxes.erase(preferredTransferSyntax);
-
-    // Flatten an array with the fallback transfer syntaxes
-    std::vector<const char*> asFallback;
-    asFallback.reserve(fallbackSyntaxes.size());
-    for (std::set<std::string>::const_iterator 
-           it = fallbackSyntaxes.begin(); it != fallbackSyntaxes.end(); ++it)
-    {
-      asFallback.push_back(it->c_str());
-    }
-
-    CheckStorageSOPClassesInvariant();
-    unsigned int presentationContextId = 1;
-
-    for (std::list<std::string>::const_iterator it = reservedStorageSOPClasses_.begin();
-         it != reservedStorageSOPClasses_.end(); ++it)
-    {
-      RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
-                              *it, asPreferred, asFallback);
-    }
-
-    for (std::set<std::string>::const_iterator it = storageSOPClasses_.begin();
-         it != storageSOPClasses_.end(); ++it)
-    {
-      RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
-                              *it, asPreferred, asFallback);
-    }
-
-    for (std::set<std::string>::const_iterator it = defaultStorageSOPClasses_.begin();
-         it != defaultStorageSOPClasses_.end(); ++it)
-    {
-      RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
-                              *it, asPreferred, asFallback);
-    }
-  }
-
-
-  static bool IsGenericTransferSyntax(const std::string& syntax)
-  {
-    return (syntax == UID_LittleEndianExplicitTransferSyntax ||
-            syntax == UID_BigEndianExplicitTransferSyntax ||
-            syntax == UID_LittleEndianImplicitTransferSyntax);
-  }
-
-
-  void DicomUserConnection::PImpl::Store(DcmInputStream& is, DicomUserConnection& connection)
-  {
-    CheckIsOpen();
-
-    DcmFileFormat dcmff;
-    Check(dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength));
-
-    // Determine the storage SOP class UID for this instance
-    static const DcmTagKey DCM_SOP_CLASS_UID(0x0008, 0x0016);
-    OFString sopClassUid;
-    if (dcmff.getDataset()->findAndGetOFString(DCM_SOP_CLASS_UID, sopClassUid).good())
-    {
-      connection.AddStorageSOPClass(sopClassUid.c_str());
-    }
-
-    // Determine whether a new presentation context must be
-    // negotiated, depending on the transfer syntax of this instance
-    DcmXfer xfer(dcmff.getDataset()->getOriginalXfer());
-    const std::string syntax(xfer.getXferID());
-    bool isGeneric = IsGenericTransferSyntax(syntax);
-
-    bool renegociate;
-    if (isGeneric)
-    {
-      // Are we making a generic-to-specific or specific-to-generic change of
-      // the transfer syntax? If this is the case, renegotiate the connection.
-      renegociate = !IsGenericTransferSyntax(connection.GetPreferredTransferSyntax());
-    }
-    else
-    {
-      // We are using a specific transfer syntax. Renegociate if the
-      // current connection does not match this transfer syntax.
-      renegociate = (syntax != connection.GetPreferredTransferSyntax());
-    }
-
-    if (renegociate)
-    {
-      LOG(INFO) << "Change in the transfer syntax: the C-Store associated must be renegotiated";
-
-      if (isGeneric)
-      {
-        connection.ResetPreferredTransferSyntax();
-      }
-      else
-      {
-        connection.SetPreferredTransferSyntax(syntax);
-      }
-    }
-
-    if (!connection.IsOpen())
-    {
-      LOG(INFO) << "Renegotiating a C-Store association due to a change in the parameters";
-      connection.Open();
-    }
-
-    // Figure out which SOP class and SOP instance is encapsulated in the file
-    DIC_UI sopClass;
-    DIC_UI sopInstance;
-    if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClass, sopInstance))
-    {
-      throw OrthancException(ErrorCode_NoSopClassOrInstance);
-    }
-
-    // Figure out which of the accepted presentation contexts should be used
-    int presID = ASC_findAcceptedPresentationContextID(assoc_, sopClass);
-    if (presID == 0)
-    {
-      const char *modalityName = dcmSOPClassUIDToModality(sopClass);
-      if (!modalityName) modalityName = dcmFindNameOfUID(sopClass);
-      if (!modalityName) modalityName = "unknown SOP class";
-      throw OrthancException(ErrorCode_NoPresentationContext);
-    }
-
-    // Prepare the transmission of data
-    T_DIMSE_C_StoreRQ req;
-    memset(&req, 0, sizeof(req));
-    req.MessageID = assoc_->nextMsgID++;
-    strcpy(req.AffectedSOPClassUID, sopClass);
-    strcpy(req.AffectedSOPInstanceUID, sopInstance);
-    req.DataSetType = DIMSE_DATASET_PRESENT;
-    req.Priority = DIMSE_PRIORITY_MEDIUM;
-
-    // Finally conduct transmission of data
-    T_DIMSE_C_StoreRSP rsp;
-    DcmDataset* statusDetail = NULL;
-    Check(DIMSE_storeUser(assoc_, presID, &req,
-                          NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL,
-                          /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ dimseTimeout_,
-                          &rsp, &statusDetail, NULL));
-
-    if (statusDetail != NULL) 
-    {
-      delete statusDetail;
-    }
-  }
-
-
-  namespace
-  {
-    struct FindPayload
-    {
-      DicomFindAnswers* answers;
-      std::string       level;
-    };
-  }
-
-
-  static void FindCallback(
-    /* in */
-    void *callbackData,
-    T_DIMSE_C_FindRQ *request,      /* original find request */
-    int responseCount,
-    T_DIMSE_C_FindRSP *response,    /* pending response received */
-    DcmDataset *responseIdentifiers /* pending response identifiers */
-    )
-  {
-    FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData);
-
-    if (responseIdentifiers != NULL)
-    {
-      DicomMap m;
-      FromDcmtkBridge::Convert(m, *responseIdentifiers);
-
-      if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
-      {
-        m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level);
-      }
-
-      payload.answers->Add(m);
-    }
-  }
-
-
-  static void FixFindQuery(DicomMap& fixedQuery,
-                           ResourceType level,
-                           const DicomMap& fields)
-  {
-    std::set<DicomTag> allowedTags;
-
-    // WARNING: Do not add "break" or reorder items in this switch-case!
-    switch (level)
-    {
-      case ResourceType_Instance:
-        DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance);
-
-      case ResourceType_Series:
-        DicomTag::AddTagsForModule(allowedTags, DicomModule_Series);
-
-      case ResourceType_Study:
-        DicomTag::AddTagsForModule(allowedTags, DicomModule_Study);
-
-      case ResourceType_Patient:
-        DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (level == ResourceType_Study)
-    {
-      allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
-    }
-
-    allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET);
-
-    DicomArray query(fields);
-    for (size_t i = 0; i < query.GetSize(); i++)
-    {
-      const DicomTag& tag = query.GetElement(i).GetTag();
-      if (allowedTags.find(tag) == allowedTags.end())
-      {
-        LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag;
-      }
-      else
-      {
-        fixedQuery.SetValue(tag, query.GetElement(i).GetValue());
-      }
-    }
-  }
-
-
-  static DcmDataset* ConvertQueryFields(const DicomMap& fields,
-                                        ModalityManufacturer manufacturer)
-  {
-    switch (manufacturer)
-    {
-      case ModalityManufacturer_SyngoVia:
-      {
-        std::auto_ptr<DicomMap> fix(fields.Clone());
-
-        // This issue for Syngo.Via and its solution was reported by
-        // Emsy Chan by private mail on June 17th, 2015.
-        std::set<DicomTag> tags;
-        fix->GetTags(tags);
-
-        for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
-        {
-          if (FromDcmtkBridge::GetValueRepresentation(*it) == ValueRepresentation_Date)
-          {
-            // Replace a "*" query by an empty query ("") for "date"
-            // value representations. Necessary to search over dates
-            // in Syngo.Via.
-            const DicomValue* value = fix->TestAndGetValue(*it);
-
-            if (value != NULL && 
-                !value->IsNull() &&
-                value->GetContent() == "*")
-            {
-              fix->SetValue(*it, "");
-            }
-          }
-        }
-
-        return ToDcmtkBridge::Convert(*fix);
-      }
-
-      default:
-        return ToDcmtkBridge::Convert(fields);
-    }
-  }
-
-
-  void DicomUserConnection::Find(DicomFindAnswers& result,
-                                 ResourceType level,
-                                 const DicomMap& originalFields)
-  {
-    DicomMap fields;
-    FixFindQuery(fields, level, originalFields);
-
-    CheckIsOpen();
-
-    FindPayload payload;
-    payload.answers = &result;
-
-    std::auto_ptr<DcmDataset> dataset(ConvertQueryFields(fields, manufacturer_));
-
-    const char* sopClass;
-    switch (level)
-    {
-      case ResourceType_Patient:
-        payload.level = "PATIENT";
-        DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "PATIENT");
-        sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
-        break;
-
-      case ResourceType_Study:
-        payload.level = "STUDY";
-        DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "STUDY");
-        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
-        break;
-
-      case ResourceType_Series:
-        payload.level = "SERIES";
-        DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "SERIES");
-        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
-        break;
-
-      case ResourceType_Instance:
-        payload.level = "INSTANCE";
-        if (manufacturer_ == ModalityManufacturer_ClearCanvas ||
-            manufacturer_ == ModalityManufacturer_Dcm4Chee)
-        {
-          // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>.
-          // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J
-          // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "IMAGE");
-        }
-        else
-        {
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "INSTANCE");
-        }
-
-        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    // Add the expected tags for this query level.
-    // WARNING: Do not reorder or add "break" in this switch-case!
-    switch (level)
-    {
-      case ResourceType_Instance:
-        // SOP Instance UID
-        if (!fields.HasTag(0x0008, 0x0018))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0018), "");
-
-      case ResourceType_Series:
-        // Series instance UID
-        if (!fields.HasTag(0x0020, 0x000e))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000e), "");
-
-      case ResourceType_Study:
-        // Accession number
-        if (!fields.HasTag(0x0008, 0x0050))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0050), "");
-
-        // Study instance UID
-        if (!fields.HasTag(0x0020, 0x000d))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000d), "");
-
-      case ResourceType_Patient:
-        // Patient ID
-        if (!fields.HasTag(0x0010, 0x0020))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0010, 0x0020), "");
-
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    // Figure out which of the accepted presentation contexts should be used
-    int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass);
-    if (presID == 0)
-    {
-      throw OrthancException(ErrorCode_DicomFindUnavailable);
-    }
-
-    T_DIMSE_C_FindRQ request;
-    memset(&request, 0, sizeof(request));
-    request.MessageID = pimpl_->assoc_->nextMsgID++;
-    strcpy(request.AffectedSOPClassUID, sopClass);
-    request.DataSetType = DIMSE_DATASET_PRESENT;
-    request.Priority = DIMSE_PRIORITY_MEDIUM;
-
-    T_DIMSE_C_FindRSP response;
-    DcmDataset* statusDetail = NULL;
-    OFCondition cond = DIMSE_findUser(pimpl_->assoc_, presID, &request, dataset.get(),
-                                      FindCallback, &payload,
-                                      /*opt_blockMode*/ DIMSE_BLOCKING, 
-                                      /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
-                                      &response, &statusDetail);
-
-    if (statusDetail)
-    {
-      delete statusDetail;
-    }
-
-    Check(cond);
-  }
-
-
-  void DicomUserConnection::MoveInternal(const std::string& targetAet,
-                                         ResourceType level,
-                                         const DicomMap& fields)
-  {
-    CheckIsOpen();
-
-    std::auto_ptr<DcmDataset> dataset(ConvertQueryFields(fields, manufacturer_));
-
-    const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
-    switch (level)
-    {
-      case ResourceType_Patient:
-        DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "PATIENT");
-        break;
-
-      case ResourceType_Study:
-        DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "STUDY");
-        break;
-
-      case ResourceType_Series:
-        DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "SERIES");
-        break;
-
-      case ResourceType_Instance:
-        if (manufacturer_ == ModalityManufacturer_ClearCanvas ||
-            manufacturer_ == ModalityManufacturer_Dcm4Chee)
-        {
-          // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>.
-          // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J
-          // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "IMAGE");
-        }
-        else
-        {
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "INSTANCE");
-        }
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    // Figure out which of the accepted presentation contexts should be used
-    int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass);
-    if (presID == 0)
-    {
-      throw OrthancException(ErrorCode_DicomMoveUnavailable);
-    }
-
-    T_DIMSE_C_MoveRQ request;
-    memset(&request, 0, sizeof(request));
-    request.MessageID = pimpl_->assoc_->nextMsgID++;
-    strcpy(request.AffectedSOPClassUID, sopClass);
-    request.DataSetType = DIMSE_DATASET_PRESENT;
-    request.Priority = DIMSE_PRIORITY_MEDIUM;
-    strncpy(request.MoveDestination, targetAet.c_str(), sizeof(DIC_AE) / sizeof(char));
-
-    T_DIMSE_C_MoveRSP response;
-    DcmDataset* statusDetail = NULL;
-    DcmDataset* responseIdentifiers = NULL;
-    OFCondition cond = DIMSE_moveUser(pimpl_->assoc_, presID, &request, dataset.get(),
-                                      NULL, NULL,
-                                      /*opt_blockMode*/ DIMSE_BLOCKING, 
-                                      /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
-                                      pimpl_->net_, NULL, NULL,
-                                      &response, &statusDetail, &responseIdentifiers);
-
-    if (statusDetail)
-    {
-      delete statusDetail;
-    }
-
-    if (responseIdentifiers)
-    {
-      delete responseIdentifiers;
-    }
-
-    Check(cond);
-  }
-
-
-  void DicomUserConnection::ResetStorageSOPClasses()
-  {
-    CheckStorageSOPClassesInvariant();
-
-    storageSOPClasses_.clear();
-    defaultStorageSOPClasses_.clear();
-
-    // Copy the short list of storage SOP classes from DCMTK, making
-    // room for the 4 SOP classes reserved for C-ECHO, C-FIND, C-MOVE.
-
-    std::set<std::string> uncommon;
-    uncommon.insert(UID_BlendingSoftcopyPresentationStateStorage);
-    uncommon.insert(UID_GrayscaleSoftcopyPresentationStateStorage);
-    uncommon.insert(UID_ColorSoftcopyPresentationStateStorage);
-    uncommon.insert(UID_PseudoColorSoftcopyPresentationStateStorage);
-
-    // Add the storage syntaxes for C-STORE
-    for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs - 1; i++)
-    {
-      if (uncommon.find(dcmShortSCUStorageSOPClassUIDs[i]) == uncommon.end())
-      {
-        defaultStorageSOPClasses_.insert(dcmShortSCUStorageSOPClassUIDs[i]);
-      }
-    }
-
-    CheckStorageSOPClassesInvariant();
-  }
-
-
-  DicomUserConnection::DicomUserConnection() : 
-    pimpl_(new PImpl),
-    preferredTransferSyntax_(DEFAULT_PREFERRED_TRANSFER_SYNTAX),
-    localAet_("STORESCU"),
-    remoteAet_("ANY-SCP"),
-    remoteHost_("127.0.0.1")
-  {
-    remotePort_ = 104;
-    manufacturer_ = ModalityManufacturer_Generic;
-
-    SetTimeout(10); 
-    pimpl_->net_ = NULL;
-    pimpl_->params_ = NULL;
-    pimpl_->assoc_ = NULL;
-
-    // SOP classes for C-ECHO, C-FIND and C-MOVE
-    reservedStorageSOPClasses_.push_back(UID_VerificationSOPClass);
-    reservedStorageSOPClasses_.push_back(UID_FINDPatientRootQueryRetrieveInformationModel);
-    reservedStorageSOPClasses_.push_back(UID_FINDStudyRootQueryRetrieveInformationModel);
-    reservedStorageSOPClasses_.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel);
-
-    ResetStorageSOPClasses();
-  }
-
-  DicomUserConnection::~DicomUserConnection()
-  {
-    Close();
-  }
-
-
-  void DicomUserConnection::SetRemoteModality(const RemoteModalityParameters& parameters)
-  {
-    SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle());
-    SetRemoteHost(parameters.GetHost());
-    SetRemotePort(parameters.GetPort());
-    SetRemoteManufacturer(parameters.GetManufacturer());
-  }
-
-
-  void DicomUserConnection::SetLocalApplicationEntityTitle(const std::string& aet)
-  {
-    if (localAet_ != aet)
-    {
-      Close();
-      localAet_ = aet;
-    }
-  }
-
-  void DicomUserConnection::SetRemoteApplicationEntityTitle(const std::string& aet)
-  {
-    if (remoteAet_ != aet)
-    {
-      Close();
-      remoteAet_ = aet;
-    }
-  }
-
-  void DicomUserConnection::SetRemoteManufacturer(ModalityManufacturer manufacturer)
-  {
-    if (manufacturer_ != manufacturer)
-    {
-      Close();
-      manufacturer_ = manufacturer;
-    }
-  }
-
-  void DicomUserConnection::ResetPreferredTransferSyntax()
-  {
-    SetPreferredTransferSyntax(DEFAULT_PREFERRED_TRANSFER_SYNTAX);
-  }
-
-  void DicomUserConnection::SetPreferredTransferSyntax(const std::string& preferredTransferSyntax)
-  {
-    if (preferredTransferSyntax_ != preferredTransferSyntax)
-    {
-      Close();
-      preferredTransferSyntax_ = preferredTransferSyntax;
-    }
-  }
-
-
-  void DicomUserConnection::SetRemoteHost(const std::string& host)
-  {
-    if (remoteHost_ != host)
-    {
-      if (host.size() > HOST_NAME_MAX - 10)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-
-      Close();
-      remoteHost_ = host;
-    }
-  }
-
-  void DicomUserConnection::SetRemotePort(uint16_t port)
-  {
-    if (remotePort_ != port)
-    {
-      Close();
-      remotePort_ = port;
-    }
-  }
-
-  void DicomUserConnection::Open()
-  {
-    if (IsOpen())
-    {
-      // Don't reopen the connection
-      return;
-    }
-
-    LOG(INFO) << "Opening a DICOM SCU connection from AET \"" << GetLocalApplicationEntityTitle() 
-              << "\" to AET \"" << GetRemoteApplicationEntityTitle() << "\" on host "
-              << GetRemoteHost() << ":" << GetRemotePort() 
-              << " (manufacturer: " << EnumerationToString(GetRemoteManufacturer()) << ")";
-
-    Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_));
-    Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU));
-
-    // Set this application's title and the called application's title in the params
-    Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), remoteAet_.c_str(), NULL));
-
-    // Set the network addresses of the local and remote entities
-    char localHost[HOST_NAME_MAX];
-    gethostname(localHost, HOST_NAME_MAX - 1);
-
-    char remoteHostAndPort[HOST_NAME_MAX];
-
-#ifdef _MSC_VER
-    _snprintf
-#else
-      snprintf
-#endif
-      (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", remoteHost_.c_str(), remotePort_);
-
-    Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, remoteHostAndPort));
-
-    // Set various options
-    Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false));
-
-    SetupPresentationContexts(preferredTransferSyntax_);
-
-    // Do the association
-    Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_));
-
-    if (ASC_countAcceptedPresentationContexts(pimpl_->params_) == 0)
-    {
-      throw OrthancException(ErrorCode_NoPresentationContext);
-    }
-  }
-
-  void DicomUserConnection::Close()
-  {
-    if (pimpl_->assoc_ != NULL)
-    {
-      ASC_releaseAssociation(pimpl_->assoc_);
-      ASC_destroyAssociation(&pimpl_->assoc_);
-      pimpl_->assoc_ = NULL;
-      pimpl_->params_ = NULL;
-    }
-    else
-    {
-      if (pimpl_->params_ != NULL)
-      {
-        ASC_destroyAssociationParameters(&pimpl_->params_);
-        pimpl_->params_ = NULL;
-      }
-    }
-
-    if (pimpl_->net_ != NULL)
-    {
-      ASC_dropNetwork(&pimpl_->net_);
-      pimpl_->net_ = NULL;
-    }
-  }
-
-  bool DicomUserConnection::IsOpen() const
-  {
-    return pimpl_->IsOpen();
-  }
-
-  void DicomUserConnection::Store(const char* buffer, size_t size)
-  {
-    // Prepare an input stream for the memory buffer
-    DcmInputBufferStream is;
-    if (size > 0)
-      is.setBuffer(buffer, size);
-    is.setEos();
-      
-    pimpl_->Store(is, *this);
-  }
-
-  void DicomUserConnection::Store(const std::string& buffer)
-  {
-    if (buffer.size() > 0)
-      Store(reinterpret_cast<const char*>(&buffer[0]), buffer.size());
-    else
-      Store(NULL, 0);
-  }
-
-  void DicomUserConnection::StoreFile(const std::string& path)
-  {
-    // Prepare an input stream for the file
-    DcmInputFileStream is(path.c_str());
-    pimpl_->Store(is, *this);
-  }
-
-  bool DicomUserConnection::Echo()
-  {
-    CheckIsOpen();
-    DIC_US status;
-    Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++, 
-                         /*opt_blockMode*/ DIMSE_BLOCKING, 
-                         /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
-                         &status, NULL));
-    return status == STATUS_Success;
-  }
-
-
-  static void TestAndCopyTag(DicomMap& result,
-                             const DicomMap& source,
-                             const DicomTag& tag)
-  {
-    if (!source.HasTag(tag))
-    {
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-    else
-    {
-      result.SetValue(tag, source.GetValue(tag));
-    }
-  }
-
-
-  void DicomUserConnection::Move(const std::string& targetAet,
-                                 const DicomMap& findResult)
-  {
-    if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
-    ResourceType level = StringToResourceType(tmp.c_str());
-
-    DicomMap move;
-    switch (level)
-    {
-      case ResourceType_Patient:
-        TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID);
-        break;
-
-      case ResourceType_Study:
-        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
-        break;
-
-      case ResourceType_Series:
-        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
-        TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
-        break;
-
-      case ResourceType_Instance:
-        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
-        TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
-        TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    MoveInternal(targetAet, level, move);
-  }
-
-
-  void DicomUserConnection::MovePatient(const std::string& targetAet,
-                                        const std::string& patientId)
-  {
-    DicomMap query;
-    query.SetValue(DICOM_TAG_PATIENT_ID, patientId);
-    MoveInternal(targetAet, ResourceType_Patient, query);
-  }
-
-  void DicomUserConnection::MoveStudy(const std::string& targetAet,
-                                      const std::string& studyUid)
-  {
-    DicomMap query;
-    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid);
-    MoveInternal(targetAet, ResourceType_Study, query);
-  }
-
-  void DicomUserConnection::MoveSeries(const std::string& targetAet,
-                                       const std::string& studyUid,
-                                       const std::string& seriesUid)
-  {
-    DicomMap query;
-    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid);
-    query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid);
-    MoveInternal(targetAet, ResourceType_Series, query);
-  }
-
-  void DicomUserConnection::MoveInstance(const std::string& targetAet,
-                                         const std::string& studyUid,
-                                         const std::string& seriesUid,
-                                         const std::string& instanceUid)
-  {
-    DicomMap query;
-    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid);
-    query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid);
-    query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid);
-    MoveInternal(targetAet, ResourceType_Instance, query);
-  }
-
-
-  void DicomUserConnection::SetTimeout(uint32_t seconds)
-  {
-    if (seconds <= 0)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    dcmConnectionTimeout.set(seconds);
-    pimpl_->dimseTimeout_ = seconds;
-    pimpl_->acseTimeout_ = 10;
-  }
-
-
-  void DicomUserConnection::DisableTimeout()
-  {
-    /**
-     * Global timeout (seconds) for connecting to remote hosts.
-     * Default value is -1 which selects infinite timeout, i.e. blocking connect().
-     */
-    dcmConnectionTimeout.set(-1);
-    pimpl_->dimseTimeout_ = 0;
-    pimpl_->acseTimeout_ = 10;
-  }
-
-
-  void DicomUserConnection::CheckStorageSOPClassesInvariant() const
-  {
-    assert(storageSOPClasses_.size() + 
-           defaultStorageSOPClasses_.size() + 
-           reservedStorageSOPClasses_.size() <= MAXIMUM_STORAGE_SOP_CLASSES);
-  }
-
-  void DicomUserConnection::AddStorageSOPClass(const char* sop)
-  {
-    CheckStorageSOPClassesInvariant();
-
-    if (storageSOPClasses_.find(sop) != storageSOPClasses_.end())
-    {
-      // This storage SOP class is already explicitly registered. Do
-      // nothing.
-      return;
-    }
-
-    if (defaultStorageSOPClasses_.find(sop) != defaultStorageSOPClasses_.end())
-    {
-      // This storage SOP class is not explicitly registered, but is
-      // used by default. Just register it explicitly.
-      defaultStorageSOPClasses_.erase(sop);
-      storageSOPClasses_.insert(sop);
-
-      CheckStorageSOPClassesInvariant();
-      return;
-    }
-
-    // This storage SOP class is neither explicitly, nor implicitly
-    // registered. Close the connection and register it explicitly.
-
-    Close();
-
-    if (reservedStorageSOPClasses_.size() + 
-        storageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES)  // (*)
-    {
-      // The maximum number of SOP classes is reached
-      ResetStorageSOPClasses();
-      defaultStorageSOPClasses_.erase(sop);
-    }
-    else if (reservedStorageSOPClasses_.size() + storageSOPClasses_.size() + 
-             defaultStorageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES)
-    {
-      // Make room in the default storage syntaxes
-      assert(!defaultStorageSOPClasses_.empty());  // Necessarily true because condition (*) is false
-      defaultStorageSOPClasses_.erase(*defaultStorageSOPClasses_.rbegin());
-    }
-
-    // Explicitly register the new storage syntax
-    storageSOPClasses_.insert(sop);
-
-    CheckStorageSOPClassesInvariant();
-  }
-
-}
--- a/OrthancServer/DicomProtocol/DicomUserConnection.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,168 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "DicomFindAnswers.h"
-#include "../ServerEnumerations.h"
-#include "RemoteModalityParameters.h"
-
-#include <stdint.h>
-#include <boost/shared_ptr.hpp>
-#include <boost/noncopyable.hpp>
-#include <list>
-
-namespace Orthanc
-{
-  class DicomUserConnection : public boost::noncopyable
-  {
-  private:
-    struct PImpl;
-    boost::shared_ptr<PImpl> pimpl_;
-
-    // Connection parameters
-    std::string preferredTransferSyntax_;
-    std::string localAet_;
-    std::string remoteAet_;
-    std::string remoteHost_;
-    uint16_t remotePort_;
-    ModalityManufacturer manufacturer_;
-    std::set<std::string> storageSOPClasses_;
-    std::list<std::string> reservedStorageSOPClasses_;
-    std::set<std::string> defaultStorageSOPClasses_;
-
-    void CheckIsOpen() const;
-
-    void SetupPresentationContexts(const std::string& preferredTransferSyntax);
-
-    void MoveInternal(const std::string& targetAet,
-                      ResourceType level,
-                      const DicomMap& fields);
-
-    void ResetStorageSOPClasses();
-
-    void CheckStorageSOPClassesInvariant() const;
-
-  public:
-    DicomUserConnection();
-
-    ~DicomUserConnection();
-
-    void SetRemoteModality(const RemoteModalityParameters& parameters);
-
-    void SetLocalApplicationEntityTitle(const std::string& aet);
-
-    const std::string& GetLocalApplicationEntityTitle() const
-    {
-      return localAet_;
-    }
-
-    void SetRemoteApplicationEntityTitle(const std::string& aet);
-
-    const std::string& GetRemoteApplicationEntityTitle() const
-    {
-      return remoteAet_;
-    }
-
-    void SetRemoteHost(const std::string& host);
-
-    const std::string& GetRemoteHost() const
-    {
-      return remoteHost_;
-    }
-
-    void SetRemotePort(uint16_t port);
-
-    uint16_t GetRemotePort() const
-    {
-      return remotePort_;
-    }
-
-    void SetRemoteManufacturer(ModalityManufacturer manufacturer);
-
-    ModalityManufacturer GetRemoteManufacturer() const
-    {
-      return manufacturer_;
-    }
-
-    void ResetPreferredTransferSyntax();
-
-    void SetPreferredTransferSyntax(const std::string& preferredTransferSyntax);
-
-    const std::string& GetPreferredTransferSyntax() const
-    {
-      return preferredTransferSyntax_;
-    }
-
-    void AddStorageSOPClass(const char* sop);
-
-    void Open();
-
-    void Close();
-
-    bool IsOpen() const;
-
-    bool Echo();
-
-    void Store(const char* buffer, size_t size);
-
-    void Store(const std::string& buffer);
-
-    void StoreFile(const std::string& path);
-
-    void Find(DicomFindAnswers& result,
-              ResourceType level,
-              const DicomMap& fields);
-
-    void Move(const std::string& targetAet,
-              const DicomMap& findResult);
-
-    void MovePatient(const std::string& targetAet,
-                     const std::string& patientId);
-
-    void MoveStudy(const std::string& targetAet,
-                   const std::string& studyUid);
-
-    void MoveSeries(const std::string& targetAet,
-                    const std::string& studyUid,
-                    const std::string& seriesUid);
-
-    void MoveInstance(const std::string& targetAet,
-                      const std::string& studyUid,
-                      const std::string& seriesUid,
-                      const std::string& instanceUid);
-
-    void SetTimeout(uint32_t seconds);
-
-    void DisableTimeout();
-  };
-}
--- a/OrthancServer/DicomProtocol/IApplicationEntityFilter.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../ServerEnumerations.h"
-
-#include <string>
-
-namespace Orthanc
-{
-  class IApplicationEntityFilter
-  {
-  public:
-    virtual ~IApplicationEntityFilter()
-    {
-    }
-
-    virtual bool IsAllowedConnection(const std::string& callingIp,
-                                     const std::string& callingAet) = 0;
-
-    virtual bool IsAllowedRequest(const std::string& callingIp,
-                                  const std::string& callingAet,
-                                  DicomRequestType type) = 0;
-
-    virtual bool IsAllowedTransferSyntax(const std::string& callingIp,
-                                         const std::string& callingAet,
-                                         TransferSyntax syntax) = 0;
-  };
-}
--- a/OrthancServer/DicomProtocol/IFindRequestHandler.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "DicomFindAnswers.h"
-
-#include <vector>
-#include <string>
-
-
-namespace Orthanc
-{
-  class IFindRequestHandler
-  {
-  public:
-    virtual ~IFindRequestHandler()
-    {
-    }
-
-    /**
-     * Can throw exceptions. Returns "false" iff too many results have
-     * to be returned. In such a case, a "Matching terminated due to
-     * Cancel request" DIMSE code would be returned.
-     * https://www.dabsoft.ch/dicom/4/V.4.1/
-     **/
-    virtual bool Handle(DicomFindAnswers& answers,
-                        const DicomMap& input,
-                        const std::string& remoteIp,
-                        const std::string& remoteAet) = 0;
-  };
-}
--- a/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IFindRequestHandler.h"
-
-namespace Orthanc
-{
-  class IFindRequestHandlerFactory
-  {
-  public:
-    virtual ~IFindRequestHandlerFactory()
-    {
-    }
-
-    virtual IFindRequestHandler* ConstructFindRequestHandler() = 0;
-  };
-}
--- a/OrthancServer/DicomProtocol/IMoveRequestHandler.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../../Core/DicomFormat/DicomMap.h"
-
-#include <vector>
-#include <string>
-
-
-namespace Orthanc
-{
-  class IMoveRequestIterator
-  {
-  public:
-    enum Status
-    {
-      Status_Success,
-      Status_Failure,
-      Status_Warning
-    };
-
-    virtual ~IMoveRequestIterator()
-    {
-    }
-
-    virtual unsigned int GetSubOperationCount() const = 0;
-
-    virtual Status DoNext() = 0;
-  };
-
-
-  class IMoveRequestHandler
-  {
-  public:
-    virtual ~IMoveRequestHandler()
-    {
-    }
-
-    virtual IMoveRequestIterator* Handle(const std::string& target,
-                                         const DicomMap& input,
-                                         const std::string& remoteIp,
-                                         const std::string& remoteAet) = 0;
-  };
-
-}
--- a/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IMoveRequestHandler.h"
-
-namespace Orthanc
-{
-  class IMoveRequestHandlerFactory
-  {
-  public:
-    virtual ~IMoveRequestHandlerFactory()
-    {
-    }
-
-    virtual IMoveRequestHandler* ConstructMoveRequestHandler() = 0;
-  };
-}
--- a/OrthancServer/DicomProtocol/IStoreRequestHandler.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../../Core/DicomFormat/DicomMap.h"
-
-#include <vector>
-#include <string>
-#include <json/json.h>
-
-namespace Orthanc
-{
-  class IStoreRequestHandler
-  {
-  public:
-    virtual ~IStoreRequestHandler()
-    {
-    }
-
-    virtual void Handle(const std::string& dicomFile,
-                        const DicomMap& dicomSummary,
-                        const Json::Value& dicomJson,
-                        const std::string& remoteIp,
-                        const std::string& remoteAet,
-                        const std::string& calledAet) = 0;
-  };
-}
--- a/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IStoreRequestHandler.h"
-
-namespace Orthanc
-{
-  class IStoreRequestHandlerFactory
-  {
-  public:
-    virtual ~IStoreRequestHandlerFactory()
-    {
-    }
-
-    virtual IStoreRequestHandler* ConstructStoreRequestHandler() = 0;
-  };
-}
--- a/OrthancServer/DicomProtocol/RemoteModalityParameters.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "RemoteModalityParameters.h"
-
-#include "../../Core/OrthancException.h"
-
-#include <boost/lexical_cast.hpp>
-#include <stdexcept>
-
-namespace Orthanc
-{
-  RemoteModalityParameters::RemoteModalityParameters() :
-    aet_("ORTHANC"),
-    host_("localhost"),
-    port_(104),
-    manufacturer_(ModalityManufacturer_Generic)
-  {
-  }
-
-  RemoteModalityParameters::RemoteModalityParameters(const std::string& aet,
-                                                     const std::string& host,
-                                                     uint16_t port,
-                                                     ModalityManufacturer manufacturer)
-  {
-    SetApplicationEntityTitle(aet);
-    SetHost(host);
-    SetPort(port);
-    SetManufacturer(manufacturer);
-  }
-
-
-  void RemoteModalityParameters::FromJson(const Json::Value& modality)
-  {
-    if (!modality.isArray() ||
-        (modality.size() != 3 && modality.size() != 4))
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    SetApplicationEntityTitle(modality.get(0u, "").asString());
-    SetHost(modality.get(1u, "").asString());
-
-    const Json::Value& portValue = modality.get(2u, "");
-    try
-    {
-      int tmp = portValue.asInt();
-
-      if (tmp <= 0 || tmp >= 65535)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-
-      SetPort(static_cast<uint16_t>(tmp));
-    }
-    catch (std::runtime_error /* error inside JsonCpp */)
-    {
-      try
-      {
-        SetPort(boost::lexical_cast<uint16_t>(portValue.asString()));
-      }
-      catch (boost::bad_lexical_cast)
-      {
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-    }
-
-    if (modality.size() == 4)
-    {
-      SetManufacturer(modality.get(3u, "").asString());
-    }
-    else
-    {
-      SetManufacturer(ModalityManufacturer_Generic);
-    }
-  }
-
-  void RemoteModalityParameters::ToJson(Json::Value& value) const
-  {
-    value = Json::arrayValue;
-    value.append(GetApplicationEntityTitle());
-    value.append(GetHost());
-    value.append(GetPort());
-    value.append(EnumerationToString(GetManufacturer()));
-  }
-}
--- a/OrthancServer/DicomProtocol/RemoteModalityParameters.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,108 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../ServerEnumerations.h"
-
-#include <stdint.h>
-#include <string>
-#include <json/json.h>
-
-namespace Orthanc
-{
-  class RemoteModalityParameters
-  {
-  private:
-    std::string aet_;
-    std::string host_;
-    uint16_t port_;
-    ModalityManufacturer manufacturer_;
-
-  public:
-    RemoteModalityParameters();
-
-    RemoteModalityParameters(const std::string& aet,
-                             const std::string& host,
-                             uint16_t port,
-                             ModalityManufacturer manufacturer);
-
-    const std::string& GetApplicationEntityTitle() const
-    {
-      return aet_;
-    }
-
-    void SetApplicationEntityTitle(const std::string& aet)
-    {
-      aet_ = aet;
-    }
-
-    const std::string& GetHost() const
-    {
-      return host_;
-    }
-
-    void SetHost(const std::string& host)
-    {
-      host_ = host;
-    }
-    
-    uint16_t GetPort() const
-    {
-      return port_;
-    }
-
-    void SetPort(uint16_t port)
-    {
-      port_ = port;
-    }
-
-    ModalityManufacturer GetManufacturer() const
-    {
-      return manufacturer_;
-    }
-
-    void SetManufacturer(ModalityManufacturer manufacturer)
-    {
-      manufacturer_ = manufacturer;
-    }    
-
-    void SetManufacturer(const std::string& manufacturer)
-    {
-      manufacturer_ = StringToModalityManufacturer(manufacturer);
-    }
-
-    void FromJson(const Json::Value& modality);
-
-    void ToJson(Json::Value& value) const;
-  };
-}
--- a/OrthancServer/DicomProtocol/ReusableDicomUserConnection.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,187 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "ReusableDicomUserConnection.h"
-
-#include "../../Core/Logging.h"
-#include "../../Core/OrthancException.h"
-
-namespace Orthanc
-{
-  static boost::posix_time::ptime Now()
-  {
-    return boost::posix_time::microsec_clock::local_time();
-  }
-
-  void ReusableDicomUserConnection::Open(const std::string& localAet,
-                                         const RemoteModalityParameters& remote)
-  {
-    if (connection_ != NULL &&
-        connection_->GetLocalApplicationEntityTitle() == localAet &&
-        connection_->GetRemoteApplicationEntityTitle() == remote.GetApplicationEntityTitle() &&
-        connection_->GetRemoteHost() == remote.GetHost() &&
-        connection_->GetRemotePort() == remote.GetPort() &&
-        connection_->GetRemoteManufacturer() == remote.GetManufacturer())
-    {
-      // The current connection can be reused
-      LOG(INFO) << "Reusing the previous SCU connection";
-      return;
-    }
-
-    Close();
-
-    connection_ = new DicomUserConnection();
-    connection_->SetLocalApplicationEntityTitle(localAet);
-    connection_->SetRemoteModality(remote);
-    connection_->Open();
-  }
-    
-  void ReusableDicomUserConnection::Close()
-  {
-    if (connection_ != NULL)
-    {
-      delete connection_;
-      connection_ = NULL;
-    }
-  }
-
-  void ReusableDicomUserConnection::CloseThread(ReusableDicomUserConnection* that)
-  {
-    for (;;)
-    {
-      boost::this_thread::sleep(boost::posix_time::milliseconds(100));
-      if (!that->continue_)
-      {
-        //LOG(INFO) << "Finishing the thread watching the global SCU connection";
-        return;
-      }
-
-      {
-        boost::mutex::scoped_lock lock(that->mutex_);
-        if (that->connection_ != NULL &&
-            Now() >= that->lastUse_ + that->timeBeforeClose_)
-        {
-          LOG(INFO) << "Closing the global SCU connection after timeout";
-          that->Close();
-        }
-      }
-    }
-  }
-    
-
-  ReusableDicomUserConnection::Locker::Locker(ReusableDicomUserConnection& that,
-                                              const std::string& localAet,
-                                              const RemoteModalityParameters& remote) :
-    ::Orthanc::Locker(that)
-  {
-    that.Open(localAet, remote);
-    connection_ = that.connection_;    
-  }
-
-
-  DicomUserConnection& ReusableDicomUserConnection::Locker::GetConnection()
-  {
-    if (connection_ == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    return *connection_;
-  }      
-
-  ReusableDicomUserConnection::ReusableDicomUserConnection() : 
-    connection_(NULL), 
-    timeBeforeClose_(boost::posix_time::seconds(5))  // By default, close connection after 5 seconds
-  {
-    lastUse_ = Now();
-    continue_ = true;
-    closeThread_ = boost::thread(CloseThread, this);
-  }
-
-  ReusableDicomUserConnection::~ReusableDicomUserConnection()
-  {
-    if (continue_)
-    {
-      LOG(ERROR) << "INTERNAL ERROR: ReusableDicomUserConnection::Finalize() should be invoked manually to avoid mess in the destruction order!";
-      Finalize();
-    }
-  }
-
-  void ReusableDicomUserConnection::SetMillisecondsBeforeClose(uint64_t ms)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    if (ms == 0)
-    {
-      ms = 1;
-    }
-
-    timeBeforeClose_ = boost::posix_time::milliseconds(ms);
-  }
-
-  void ReusableDicomUserConnection::Lock()
-  {
-    mutex_.lock();
-  }
-
-  void ReusableDicomUserConnection::Unlock()
-  {
-    if (connection_ != NULL &&
-        connection_->GetRemoteManufacturer() == ModalityManufacturer_StoreScp)
-    {
-      // "storescp" from DCMTK has problems when reusing a
-      // connection. Always close.
-      Close();
-    }
-
-    lastUse_ = Now();
-    mutex_.unlock();
-  }
-
-  
-  void ReusableDicomUserConnection::Finalize()
-  {
-    if (continue_)
-    {
-      continue_ = false;
-
-      if (closeThread_.joinable())
-      {
-        closeThread_.join();
-      }
-
-      Close();
-    }
-  }
-}
-
--- a/OrthancServer/DicomProtocol/ReusableDicomUserConnection.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "DicomUserConnection.h"
-#include "../../Core/MultiThreading/Locker.h"
-
-#include <boost/thread.hpp>
-#include <boost/date_time/posix_time/posix_time.hpp>
-
-namespace Orthanc
-{
-  class ReusableDicomUserConnection : public ILockable
-  {
-  private:
-    boost::mutex mutex_;
-    DicomUserConnection* connection_;
-    bool continue_;
-    boost::posix_time::time_duration timeBeforeClose_;
-    boost::posix_time::ptime lastUse_;
-    boost::thread closeThread_;
-
-    void Open(const std::string& localAet,
-              const RemoteModalityParameters& remote);
-    
-    void Close();
-
-    static void CloseThread(ReusableDicomUserConnection* that);
-
-  protected:
-    virtual void Lock();
-
-    virtual void Unlock();
-    
-  public:
-    class Locker : public ::Orthanc::Locker
-    {
-    private:
-      DicomUserConnection* connection_;
-
-    public:
-      Locker(ReusableDicomUserConnection& that,
-             const std::string& localAet,
-             const RemoteModalityParameters& remote);
-
-      DicomUserConnection& GetConnection();
-    };
-
-    ReusableDicomUserConnection();
-
-    virtual ~ReusableDicomUserConnection();
-
-    void SetMillisecondsBeforeClose(uint64_t ms);
-
-    void Finalize();
-  };
-}
-
--- a/OrthancServer/ExportedResource.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/ExportedResource.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/ExportedResource.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/ExportedResource.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/FromDcmtkBridge.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1451 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-
-#include "PrecompiledHeadersServer.h"
-
-#ifndef NOMINMAX
-#define NOMINMAX
-#endif
-
-#include "Internals/DicomImageDecoder.h"
-
-#include "FromDcmtkBridge.h"
-#include "ToDcmtkBridge.h"
-#include "OrthancInitialization.h"
-#include "../Core/Logging.h"
-#include "../Core/Toolbox.h"
-#include "../Core/OrthancException.h"
-#include "../Core/Images/PngWriter.h"
-#include "../Core/Uuid.h"
-#include "../Core/DicomFormat/DicomIntegerPixelAccessor.h"
-
-#include <list>
-#include <limits>
-
-#include <boost/lexical_cast.hpp>
-#include <boost/filesystem.hpp>
-
-#include <dcmtk/dcmdata/dcchrstr.h>
-#include <dcmtk/dcmdata/dcdicent.h>
-#include <dcmtk/dcmdata/dcdict.h>
-#include <dcmtk/dcmdata/dcfilefo.h>
-#include <dcmtk/dcmdata/dcistrmb.h>
-#include <dcmtk/dcmdata/dcuid.h>
-#include <dcmtk/dcmdata/dcmetinf.h>
-#include <dcmtk/dcmdata/dcdeftag.h>
-
-#include <dcmtk/dcmdata/dcvrae.h>
-#include <dcmtk/dcmdata/dcvras.h>
-#include <dcmtk/dcmdata/dcvrcs.h>
-#include <dcmtk/dcmdata/dcvrda.h>
-#include <dcmtk/dcmdata/dcvrds.h>
-#include <dcmtk/dcmdata/dcvrdt.h>
-#include <dcmtk/dcmdata/dcvrfd.h>
-#include <dcmtk/dcmdata/dcvrfl.h>
-#include <dcmtk/dcmdata/dcvris.h>
-#include <dcmtk/dcmdata/dcvrlo.h>
-#include <dcmtk/dcmdata/dcvrlt.h>
-#include <dcmtk/dcmdata/dcvrpn.h>
-#include <dcmtk/dcmdata/dcvrsh.h>
-#include <dcmtk/dcmdata/dcvrsl.h>
-#include <dcmtk/dcmdata/dcvrss.h>
-#include <dcmtk/dcmdata/dcvrst.h>
-#include <dcmtk/dcmdata/dcvrtm.h>
-#include <dcmtk/dcmdata/dcvrui.h>
-#include <dcmtk/dcmdata/dcvrul.h>
-#include <dcmtk/dcmdata/dcvrus.h>
-#include <dcmtk/dcmdata/dcvrut.h>
-#include <dcmtk/dcmdata/dcpixel.h>
-#include <dcmtk/dcmdata/dcpixseq.h>
-#include <dcmtk/dcmdata/dcpxitem.h>
-#include <dcmtk/dcmdata/dcvrat.h>
-
-#include <dcmtk/dcmnet/dul.h>
-
-#include <boost/math/special_functions/round.hpp>
-#include <boost/algorithm/string/predicate.hpp>
-#include <dcmtk/dcmdata/dcostrmb.h>
-
-
-namespace Orthanc
-{
-  static inline uint16_t GetCharValue(char c)
-  {
-    if (c >= '0' && c <= '9')
-      return c - '0';
-    else if (c >= 'a' && c <= 'f')
-      return c - 'a' + 10;
-    else if (c >= 'A' && c <= 'F')
-      return c - 'A' + 10;
-    else
-      return 0;
-  }
-
-  static inline uint16_t GetTagValue(const char* c)
-  {
-    return ((GetCharValue(c[0]) << 12) + 
-            (GetCharValue(c[1]) << 8) + 
-            (GetCharValue(c[2]) << 4) + 
-            GetCharValue(c[3]));
-  }
-
-
-#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
-  static void LoadEmbeddedDictionary(DcmDataDictionary& dictionary,
-                                     EmbeddedResources::FileResourceId resource)
-  {
-    Toolbox::TemporaryFile tmp;
-
-    FILE* fp = fopen(tmp.GetPath().c_str(), "wb");
-    fwrite(EmbeddedResources::GetFileResourceBuffer(resource), 
-           EmbeddedResources::GetFileResourceSize(resource), 1, fp);
-    fclose(fp);
-
-    if (!dictionary.loadDictionary(tmp.GetPath().c_str()))
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-                             
-#else
-  static void LoadExternalDictionary(DcmDataDictionary& dictionary,
-                                     const std::string& directory,
-                                     const std::string& filename)
-  {
-    boost::filesystem::path p = directory;
-    p = p / filename;
-
-    LOG(WARNING) << "Loading the external DICOM dictionary " << p;
-
-    if (!dictionary.loadDictionary(p.string().c_str()))
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-                            
-#endif
-
-
-  namespace
-  {
-    class DictionaryLocker
-    {
-    private:
-      DcmDataDictionary& dictionary_;
-
-    public:
-      DictionaryLocker() : dictionary_(dcmDataDict.wrlock())
-      {
-      }
-
-      ~DictionaryLocker()
-      {
-        dcmDataDict.unlock();
-      }
-
-      DcmDataDictionary& operator*()
-      {
-        return dictionary_;
-      }
-
-      DcmDataDictionary* operator->()
-      {
-        return &dictionary_;
-      }
-    };
-  }
-
-
-  void FromDcmtkBridge::InitializeDictionary()
-  {
-    /* Disable "gethostbyaddr" (which results in memory leaks) and use raw IP addresses */
-    dcmDisableGethostbyaddr.set(OFTrue);
-
-    {
-      DictionaryLocker locker;
-
-      locker->clear();
-
-#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
-      LOG(WARNING) << "Loading the embedded dictionaries";
-      /**
-       * Do not load DICONDE dictionary, it breaks the other tags. The
-       * command "strace storescu 2>&1 |grep dic" shows that DICONDE
-       * dictionary is not loaded by storescu.
-       **/
-      //LoadEmbeddedDictionary(*locker, EmbeddedResources::DICTIONARY_DICONDE);
-
-      LoadEmbeddedDictionary(*locker, EmbeddedResources::DICTIONARY_DICOM);
-      LoadEmbeddedDictionary(*locker, EmbeddedResources::DICTIONARY_PRIVATE);
-
-#elif defined(__linux) || defined(__FreeBSD_kernel__)
-      std::string path = DCMTK_DICTIONARY_DIR;
-
-      const char* env = std::getenv(DCM_DICT_ENVIRONMENT_VARIABLE);
-      if (env != NULL)
-      {
-        path = std::string(env);
-      }
-
-      LoadExternalDictionary(*locker, path, "dicom.dic");
-      LoadExternalDictionary(*locker, path, "private.dic");
-
-#else
-#error Support your platform here
-#endif
-    }
-
-    /* make sure data dictionary is loaded */
-    if (!dcmDataDict.isDictionaryLoaded())
-    {
-      LOG(ERROR) << "No DICOM dictionary loaded, check environment variable: " << DCM_DICT_ENVIRONMENT_VARIABLE;
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    {
-      // Test the dictionary with a simple DICOM tag
-      DcmTag key(0x0010, 0x1030); // This is PatientWeight
-      if (key.getEVR() != EVR_DS)
-      {
-        LOG(ERROR) << "The DICOM dictionary has not been correctly read";
-        throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-  }
-
-
-  void FromDcmtkBridge::RegisterDictionaryTag(const DicomTag& tag,
-                                              const DcmEVR& vr,
-                                              const std::string& name,
-                                              unsigned int minMultiplicity,
-                                              unsigned int maxMultiplicity)
-  {
-    if (minMultiplicity < 1)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    if (maxMultiplicity == 0)
-    {
-      maxMultiplicity = DcmVariableVM;
-    }
-    else if (maxMultiplicity < minMultiplicity)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    
-    std::auto_ptr<DcmDictEntry>  entry(new DcmDictEntry(tag.GetGroup(),
-                                                        tag.GetElement(),
-                                                        vr, name.c_str(),
-                                                        static_cast<int>(minMultiplicity),
-                                                        static_cast<int>(maxMultiplicity),
-                                                        NULL    /* version */,
-                                                        OFTrue  /* doCopyString */,
-                                                        NULL    /* private creator */));
-
-    entry->setGroupRangeRestriction(DcmDictRange_Unspecified);
-    entry->setElementRangeRestriction(DcmDictRange_Unspecified);
-
-    {
-      DictionaryLocker locker;
-      locker->addEntry(entry.release());
-    }
-  }
-
-
-  Encoding FromDcmtkBridge::DetectEncoding(DcmDataset& dataset)
-  {
-    // By default, Latin1 encoding is assumed
-    std::string s = Configuration::GetGlobalStringParameter("DefaultEncoding", "Latin1");
-    Encoding encoding = s.empty() ? Encoding_Latin1 : StringToEncoding(s.c_str());
-
-    OFString tmp;
-    if (dataset.findAndGetOFString(DCM_SpecificCharacterSet, tmp).good())
-    {
-      std::string characterSet = Toolbox::StripSpaces(std::string(tmp.c_str()));
-
-      if (characterSet.empty())
-      {
-        // Empty specific character set tag: Use the default encoding
-      }
-      else if (GetDicomEncoding(encoding, characterSet.c_str()))
-      {
-        // The specific character set is supported by the Orthanc core
-      }
-      else
-      {
-        LOG(WARNING) << "Value of Specific Character Set (0008,0005) is not supported: " << characterSet
-                     << ", fallback to ASCII (remove all special characters)";
-        encoding = Encoding_Ascii;
-      }
-    }
-    else
-    {
-      // No specific character set tag: Use the default encoding
-    }
-
-    return encoding;
-  }
-
-
-  void FromDcmtkBridge::Convert(DicomMap& target, DcmDataset& dataset)
-  {
-    Encoding encoding = DetectEncoding(dataset);
-
-    target.Clear();
-    for (unsigned long i = 0; i < dataset.card(); i++)
-    {
-      DcmElement* element = dataset.getElement(i);
-      if (element && element->isLeaf())
-      {
-        target.SetValue(element->getTag().getGTag(),
-                        element->getTag().getETag(),
-                        ConvertLeafElement(*element, DicomToJsonFlags_Default, encoding));
-      }
-    }
-  }
-
-
-  DicomTag FromDcmtkBridge::Convert(const DcmTag& tag)
-  {
-    return DicomTag(tag.getGTag(), tag.getETag());
-  }
-
-
-  DicomTag FromDcmtkBridge::GetTag(const DcmElement& element)
-  {
-    return DicomTag(element.getGTag(), element.getETag());
-  }
-
-
-  bool FromDcmtkBridge::IsPrivateTag(const DicomTag& tag)
-  {
-#if 1
-    DcmTagKey tmp(tag.GetGroup(), tag.GetElement());
-    return tmp.isPrivate();
-#else
-    // Implementation for Orthanc versions <= 0.8.5
-    DcmTag tmp(tag.GetGroup(), tag.GetElement());
-    return IsPrivateTag(tmp);
-#endif
-  }
-
-
-  DicomValue* FromDcmtkBridge::ConvertLeafElement(DcmElement& element,
-                                                  DicomToJsonFlags flags,
-                                                  Encoding encoding)
-  {
-    if (!element.isLeaf())
-    {
-      // This function is only applicable to leaf elements
-      throw OrthancException(ErrorCode_BadParameterType);
-    }
-
-    if (element.isaString())
-    {
-      char *c;
-      if (element.getString(c).good())
-      {
-        if (c == NULL)  // This case corresponds to the empty string
-        {
-          return new DicomValue("", false);
-        }
-        else
-        {
-          std::string s(c);
-          std::string utf8 = Toolbox::ConvertToUtf8(s, encoding);
-          return new DicomValue(utf8, false);
-        }
-      }
-      else
-      {
-        return new DicomValue;
-      }
-    }
-
-    try
-    {
-      // http://support.dcmtk.org/docs/dcvr_8h-source.html
-      switch (element.getVR())
-      {
-
-        /**
-         * Deal with binary data (including PixelData).
-         **/
-
-        case EVR_OB:  // other byte
-        case EVR_OF:  // other float
-        case EVR_OW:  // other word
-        case EVR_UN:  // unknown value representation
-        case EVR_ox:  // OB or OW depending on context
-        {
-          if (!(flags & DicomToJsonFlags_ConvertBinaryToNull))
-          {
-            Uint8* data = NULL;
-            if (element.getUint8Array(data) == EC_Normal)
-            {
-              return new DicomValue(reinterpret_cast<const char*>(data), element.getLength(), true);
-            }
-          }
-
-          return new DicomValue;
-        }
-    
-          /**
-           * String types, should never happen at this point because of
-           * "element.isaString()".
-           **/
-      
-        case EVR_DS:  // decimal string
-        case EVR_IS:  // integer string
-        case EVR_AS:  // age string
-        case EVR_DA:  // date string
-        case EVR_DT:  // date time string
-        case EVR_TM:  // time string
-        case EVR_AE:  // application entity title
-        case EVR_CS:  // code string
-        case EVR_SH:  // short string
-        case EVR_LO:  // long string
-        case EVR_ST:  // short text
-        case EVR_LT:  // long text
-        case EVR_UT:  // unlimited text
-        case EVR_PN:  // person name
-        case EVR_UI:  // unique identifier
-          return new DicomValue;
-
-
-          /**
-           * Numberic types
-           **/ 
-      
-        case EVR_SL:  // signed long
-        {
-          Sint32 f;
-          if (dynamic_cast<DcmSignedLong&>(element).getSint32(f).good())
-            return new DicomValue(boost::lexical_cast<std::string>(f), false);
-          else
-            return new DicomValue;
-        }
-
-        case EVR_SS:  // signed short
-        {
-          Sint16 f;
-          if (dynamic_cast<DcmSignedShort&>(element).getSint16(f).good())
-            return new DicomValue(boost::lexical_cast<std::string>(f), false);
-          else
-            return new DicomValue;
-        }
-
-        case EVR_UL:  // unsigned long
-        {
-          Uint32 f;
-          if (dynamic_cast<DcmUnsignedLong&>(element).getUint32(f).good())
-            return new DicomValue(boost::lexical_cast<std::string>(f), false);
-          else
-            return new DicomValue;
-        }
-
-        case EVR_US:  // unsigned short
-        {
-          Uint16 f;
-          if (dynamic_cast<DcmUnsignedShort&>(element).getUint16(f).good())
-            return new DicomValue(boost::lexical_cast<std::string>(f), false);
-          else
-            return new DicomValue;
-        }
-
-        case EVR_FL:  // float single-precision
-        {
-          Float32 f;
-          if (dynamic_cast<DcmFloatingPointSingle&>(element).getFloat32(f).good())
-            return new DicomValue(boost::lexical_cast<std::string>(f), false);
-          else
-            return new DicomValue;
-        }
-
-        case EVR_FD:  // float double-precision
-        {
-          Float64 f;
-          if (dynamic_cast<DcmFloatingPointDouble&>(element).getFloat64(f).good())
-            return new DicomValue(boost::lexical_cast<std::string>(f), false);
-          else
-            return new DicomValue;
-        }
-
-
-        /**
-         * Attribute tag.
-         **/
-
-        case EVR_AT:
-        {
-          DcmTagKey tag;
-          if (dynamic_cast<DcmAttributeTag&>(element).getTagVal(tag, 0).good())
-          {
-            DicomTag t(tag.getGroup(), tag.getElement());
-            return new DicomValue(t.Format(), false);
-          }
-          else
-          {
-            return new DicomValue;
-          }
-        }
-
-
-        /**
-         * Sequence types, should never occur at this point because of
-         * "element.isLeaf()".
-         **/
-
-        case EVR_SQ:  // sequence of items
-          return new DicomValue;
-
-
-          /**
-           * Internal to DCMTK.
-           **/ 
-
-        case EVR_xs:  // SS or US depending on context
-        case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
-        case EVR_na:  // na="not applicable", for data which has no VR
-        case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
-        case EVR_item:  // used internally for items
-        case EVR_metainfo:  // used internally for meta info datasets
-        case EVR_dataset:  // used internally for datasets
-        case EVR_fileFormat:  // used internally for DICOM files
-        case EVR_dicomDir:  // used internally for DICOMDIR objects
-        case EVR_dirRecord:  // used internally for DICOMDIR records
-        case EVR_pixelSQ:  // used internally for pixel sequences in a compressed image
-        case EVR_pixelItem:  // used internally for pixel items in a compressed image
-        case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
-        case EVR_PixelData:  // used internally for uncompressed pixeld data
-        case EVR_OverlayData:  // used internally for overlay data
-        case EVR_UNKNOWN2B:  // used internally for elements with unknown VR with 2-byte length field in explicit VR
-          return new DicomValue;
-
-
-          /**
-           * Default case.
-           **/ 
-
-        default:
-          return new DicomValue;
-      }
-    }
-    catch (boost::bad_lexical_cast)
-    {
-      return new DicomValue;
-    }
-    catch (std::bad_cast)
-    {
-      return new DicomValue;
-    }
-  }
-
-
-  static Json::Value& PrepareNode(Json::Value& parent,
-                                  DcmElement& element,
-                                  DicomToJsonFormat format)
-  {
-    assert(parent.type() == Json::objectValue);
-
-    DicomTag tag(FromDcmtkBridge::GetTag(element));
-    const std::string formattedTag = tag.Format();
-
-    if (format == DicomToJsonFormat_Short)
-    {
-      parent[formattedTag] = Json::nullValue;
-      return parent[formattedTag];
-    }
-
-    // This code gives access to the name of the private tags
-    DcmTag tagbis(element.getTag());
-    const std::string tagName(tagbis.getTagName());      
-    
-    switch (format)
-    {
-      case DicomToJsonFormat_Simple:
-        parent[tagName] = Json::nullValue;
-        return parent[tagName];
-
-      case DicomToJsonFormat_Full:
-      {
-        parent[formattedTag] = Json::objectValue;
-        Json::Value& node = parent[formattedTag];
-
-        if (element.isLeaf())
-        {
-          node["Name"] = tagName;
-
-          if (tagbis.getPrivateCreator() != NULL)
-          {
-            node["PrivateCreator"] = tagbis.getPrivateCreator();
-          }
-
-          return node;
-        }
-        else
-        {
-          node["Name"] = tagName;
-          node["Type"] = "Sequence";
-          node["Value"] = Json::nullValue;
-          return node["Value"];
-        }
-      }
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  static void LeafValueToJson(Json::Value& target,
-                              const DicomValue& value,
-                              DicomToJsonFormat format,
-                              DicomToJsonFlags flags,
-                              unsigned int maxStringLength)
-  {
-    Json::Value* targetValue = NULL;
-    Json::Value* targetType = NULL;
-
-    switch (format)
-    {
-      case DicomToJsonFormat_Short:
-      case DicomToJsonFormat_Simple:
-      {
-        assert(target.type() == Json::nullValue);
-        targetValue = &target;
-        break;
-      }      
-
-      case DicomToJsonFormat_Full:
-      {
-        assert(target.type() == Json::objectValue);
-        target["Value"] = Json::nullValue;
-        target["Type"] = Json::nullValue;
-        targetType = &target["Type"];
-        targetValue = &target["Value"];
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    assert(targetValue != NULL);
-    assert(targetValue->type() == Json::nullValue);
-    assert(targetType == NULL || targetType->type() == Json::nullValue);
-
-    if (value.IsNull())
-    {
-      if (targetType != NULL)
-      {
-        *targetType = "Null";
-      }
-    }
-    else if (value.IsBinary())
-    {
-      if (flags & DicomToJsonFlags_ConvertBinaryToAscii)
-      {
-        *targetValue = Toolbox::ConvertToAscii(value.GetContent());
-      }
-      else
-      {
-        std::string s;
-        value.FormatDataUriScheme(s);
-        *targetValue = s;
-      }
-
-      if (targetType != NULL)
-      {
-        *targetType = "Binary";
-      }
-    }
-    else if (maxStringLength == 0 ||
-             value.GetContent().size() <= maxStringLength)
-    {
-      *targetValue = value.GetContent();
-
-      if (targetType != NULL)
-      {
-        *targetType = "String";
-      }
-    }
-    else
-    {
-      if (targetType != NULL)
-      {
-        *targetType = "TooLong";
-      }
-    }
-  }                              
-
-
-  static void DatasetToJson(Json::Value& parent,
-                            DcmItem& item,
-                            DicomToJsonFormat format,
-                            DicomToJsonFlags flags,
-                            unsigned int maxStringLength,
-                            Encoding encoding);
-
-
-  void FromDcmtkBridge::ToJson(Json::Value& parent,
-                               DcmElement& element,
-                               DicomToJsonFormat format,
-                               DicomToJsonFlags flags,
-                               unsigned int maxStringLength,
-                               Encoding encoding)
-  {
-    if (parent.type() == Json::nullValue)
-    {
-      parent = Json::objectValue;
-    }
-
-    assert(parent.type() == Json::objectValue);
-    Json::Value& target = PrepareNode(parent, element, format);
-
-    if (element.isLeaf())
-    {
-      std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(element, flags, encoding));
-      LeafValueToJson(target, *v, format, flags, maxStringLength);
-    }
-    else
-    {
-      assert(target.type() == Json::nullValue);
-      target = Json::arrayValue;
-
-      // "All subclasses of DcmElement except for DcmSequenceOfItems
-      // are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset
-      // etc. are not." The following dynamic_cast is thus OK.
-      DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(element);
-
-      for (unsigned long i = 0; i < sequence.card(); i++)
-      {
-        DcmItem* child = sequence.getItem(i);
-        Json::Value& v = target.append(Json::objectValue);
-        DatasetToJson(v, *child, format, flags, maxStringLength, encoding);
-      }
-    }
-  }
-
-
-  static void DatasetToJson(Json::Value& parent,
-                            DcmItem& item,
-                            DicomToJsonFormat format,
-                            DicomToJsonFlags flags,
-                            unsigned int maxStringLength,
-                            Encoding encoding)
-  {
-    assert(parent.type() == Json::objectValue);
-
-    for (unsigned long i = 0; i < item.card(); i++)
-    {
-      DcmElement* element = item.getElement(i);
-      if (element == NULL)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      if (!(flags & DicomToJsonFlags_IncludePrivateTags) &&
-          element->getTag().isPrivate())
-      {
-        continue;
-      }
-
-      if (!(flags & DicomToJsonFlags_IncludeUnknownTags))
-      {
-        DictionaryLocker locker;
-        if (locker->findEntry(element->getTag(), NULL) == NULL)
-        {
-          continue;
-        }
-      }
-
-      DcmEVR evr = element->getTag().getEVR();
-      if (evr == EVR_OB ||
-          evr == EVR_OF ||
-          evr == EVR_OW ||
-          evr == EVR_UN ||
-          evr == EVR_ox)
-      {
-        // This is a binary tag
-        DicomTag tag(FromDcmtkBridge::Convert(element->getTag()));
-
-        if ((tag == DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludePixelData)) ||
-            (tag != DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludeBinary)))
-        {
-          continue;
-        }
-      }
-
-      FromDcmtkBridge::ToJson(parent, *element, format, flags, maxStringLength, encoding);
-    }
-  }
-
-
-  void FromDcmtkBridge::ToJson(Json::Value& target, 
-                               DcmDataset& dataset,
-                               DicomToJsonFormat format,
-                               DicomToJsonFlags flags,
-                               unsigned int maxStringLength)
-  {
-    target = Json::objectValue;
-    DatasetToJson(target, dataset, format, flags, maxStringLength, DetectEncoding(dataset));
-  }
-
-
-  std::string FromDcmtkBridge::GetName(const DicomTag& t)
-  {
-    // Some patches for important tags because of different DICOM
-    // dictionaries between DCMTK versions
-    std::string n = t.GetMainTagsName();
-    if (n.size() != 0)
-    {
-      return n;
-    }
-    // End of patches
-
-#if 0
-    DcmTagKey tag(t.GetGroup(), t.GetElement());
-    const DcmDataDictionary& dict = dcmDataDict.rdlock();
-    const DcmDictEntry* entry = dict.findEntry(tag, NULL);
-
-    std::string s(DcmTag_ERROR_TagName);
-    if (entry != NULL)
-    {
-      s = std::string(entry->getTagName());
-    }
-
-    dcmDataDict.unlock();
-    return s;
-#else
-    DcmTag tag(t.GetGroup(), t.GetElement());
-    const char* name = tag.getTagName();
-    if (name == NULL)
-    {
-      return DcmTag_ERROR_TagName;
-    }
-    else
-    {
-      return std::string(name);
-    }
-#endif
-  }
-
-
-  DicomTag FromDcmtkBridge::ParseTag(const char* name)
-  {
-    if (strlen(name) == 9 &&
-        isxdigit(name[0]) &&
-        isxdigit(name[1]) &&
-        isxdigit(name[2]) &&
-        isxdigit(name[3]) &&
-        (name[4] == '-' || name[4] == ',') &&
-        isxdigit(name[5]) &&
-        isxdigit(name[6]) &&
-        isxdigit(name[7]) &&
-        isxdigit(name[8]))        
-    {
-      uint16_t group = GetTagValue(name);
-      uint16_t element = GetTagValue(name + 5);
-      return DicomTag(group, element);
-    }
-
-#if 0
-    const DcmDataDictionary& dict = dcmDataDict.rdlock();
-    const DcmDictEntry* entry = dict.findEntry(name);
-
-    if (entry == NULL)
-    {
-      dcmDataDict.unlock();
-      throw OrthancException(ErrorCode_UnknownDicomTag);
-    }
-    else
-    {
-      DcmTagKey key = entry->getKey();
-      DicomTag tag(key.getGroup(), key.getElement());
-      dcmDataDict.unlock();
-      return tag;
-    }
-#else
-    DcmTag tag;
-    if (DcmTag::findTagFromName(name, tag).good())
-    {
-      return DicomTag(tag.getGTag(), tag.getETag());
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_UnknownDicomTag);
-    }
-#endif
-  }
-
-
-  bool FromDcmtkBridge::IsUnknownTag(const DicomTag& tag)
-  {
-    DcmTag tmp(tag.GetGroup(), tag.GetElement());
-    return tmp.isUnknownVR();
-  }
-
-
-  void FromDcmtkBridge::ToJson(Json::Value& result,
-                               const DicomMap& values,
-                               bool simplify)
-  {
-    if (result.type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadParameterType);
-    }
-
-    result.clear();
-
-    for (DicomMap::Map::const_iterator 
-           it = values.map_.begin(); it != values.map_.end(); ++it)
-    {
-      if (simplify)
-      {
-        if (it->second->IsNull())
-        {
-          result[GetName(it->first)] = Json::nullValue;
-        }
-        else
-        {
-          // TODO IsBinary
-          result[GetName(it->first)] = it->second->GetContent();
-        }
-      }
-      else
-      {
-        Json::Value value = Json::objectValue;
-
-        value["Name"] = GetName(it->first);
-
-        if (it->second->IsNull())
-        {
-          value["Type"] = "Null";
-          value["Value"] = Json::nullValue;
-        }
-        else
-        {
-          // TODO IsBinary
-          value["Type"] = "String";
-          value["Value"] = it->second->GetContent();
-        }
-
-        result[it->first.Format()] = value;
-      }
-    }
-  }
-
-
-  std::string FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType level)
-  {
-    char uid[100];
-
-    switch (level)
-    {
-      case ResourceType_Patient:
-        // The "PatientID" field is of type LO (Long String), 64
-        // Bytes Maximum. An UUID is of length 36, thus it can be used
-        // as a random PatientID.
-        return Toolbox::GenerateUuid();
-
-      case ResourceType_Instance:
-        return dcmGenerateUniqueIdentifier(uid, SITE_INSTANCE_UID_ROOT);
-
-      case ResourceType_Series:
-        return dcmGenerateUniqueIdentifier(uid, SITE_SERIES_UID_ROOT);
-
-      case ResourceType_Study:
-        return dcmGenerateUniqueIdentifier(uid, SITE_STUDY_UID_ROOT);
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-  bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
-                                           DcmDataset& dataSet)
-  {
-    // Determine the transfer syntax which shall be used to write the
-    // information to the file. We always switch to the Little Endian
-    // syntax, with explicit length.
-
-    // http://support.dcmtk.org/docs/dcxfer_8h-source.html
-
-
-    /**
-     * Note that up to Orthanc 0.7.1 (inclusive), the
-     * "EXS_LittleEndianExplicit" was always used to save the DICOM
-     * dataset into memory. We now keep the original transfer syntax
-     * (if available).
-     **/
-    E_TransferSyntax xfer = dataSet.getOriginalXfer();
-    if (xfer == EXS_Unknown)
-    {
-      // No information about the original transfer syntax: This is
-      // most probably a DICOM dataset that was read from memory.
-      xfer = EXS_LittleEndianExplicit;
-    }
-
-    E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength;
-
-    // Create the meta-header information
-    DcmFileFormat ff(&dataSet);
-    ff.validateMetaInfo(xfer);
-    ff.removeInvalidGroups();
-
-    // Create a memory buffer with the proper size
-    uint32_t s = ff.calcElementLength(xfer, encodingType);
-    buffer.resize(s);
-    DcmOutputBufferStream ob(&buffer[0], s);
-
-    // Fill the memory buffer with the meta-header and the dataset
-    ff.transferInit();
-    OFCondition c = ff.write(ob, xfer, encodingType, NULL,
-                             /*opt_groupLength*/ EGL_recalcGL,
-                             /*opt_paddingType*/ EPD_withoutPadding);
-    ff.transferEnd();
-
-    // Handle errors
-    if (c.good())
-    {
-      return true;
-    }
-    else
-    {
-      buffer.clear();
-      return false;
-    }
-  }
-
-
-  ValueRepresentation FromDcmtkBridge::GetValueRepresentation(const DicomTag& tag)
-  {
-    DcmTag t(tag.GetGroup(), tag.GetElement());
-    switch (t.getEVR())
-    {
-      case EVR_PN:
-        return ValueRepresentation_PatientName;
-
-      case EVR_DA:
-        return ValueRepresentation_Date;
-
-      case EVR_DT:
-        return ValueRepresentation_DateTime;
-
-      case EVR_TM:
-        return ValueRepresentation_Time;
-
-      default:
-        return ValueRepresentation_Other;
-    }
-  }
-
-
-  static bool IsBinaryTag(const DcmTag& key)
-  {
-    return (key.isPrivate() || 
-            key.isUnknownVR() || 
-            key.getEVR() == EVR_OB ||
-            key.getEVR() == EVR_OF ||
-            key.getEVR() == EVR_OW ||
-            key.getEVR() == EVR_UN ||
-            key.getEVR() == EVR_ox);
-  }
-
-
-  DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag)
-  {
-    DcmTag key(tag.GetGroup(), tag.GetElement());
-
-    if (IsBinaryTag(key))
-    {
-      return new DcmOtherByteOtherWord(key);
-    }
-
-    switch (key.getEVR())
-    {
-      // http://support.dcmtk.org/docs/dcvr_8h-source.html
-
-      /**
-       * Binary types, handled above
-       **/
-    
-      case EVR_OB:  // other byte
-      case EVR_OF:  // other float
-      case EVR_OW:  // other word
-      case EVR_UN:  // unknown value representation
-      case EVR_ox:  // OB or OW depending on context
-        throw OrthancException(ErrorCode_InternalError);
-
-
-      /**
-       * String types.
-       * http://support.dcmtk.org/docs/classDcmByteString.html
-       **/
-      
-      case EVR_AS:  // age string
-        return new DcmAgeString(key);
-
-      case EVR_AE:  // application entity title
-        return new DcmApplicationEntity(key);
-
-      case EVR_CS:  // code string
-        return new DcmCodeString(key);        
-
-      case EVR_DA:  // date string
-        return new DcmDate(key);
-        
-      case EVR_DT:  // date time string
-        return new DcmDateTime(key);
-
-      case EVR_DS:  // decimal string
-        return new DcmDecimalString(key);
-
-      case EVR_IS:  // integer string
-        return new DcmIntegerString(key);
-
-      case EVR_TM:  // time string
-        return new DcmTime(key);
-
-      case EVR_UI:  // unique identifier
-        return new DcmUniqueIdentifier(key);
-
-      case EVR_ST:  // short text
-        return new DcmShortText(key);
-
-      case EVR_LO:  // long string
-        return new DcmLongString(key);
-
-      case EVR_LT:  // long text
-        return new DcmLongText(key);
-
-      case EVR_UT:  // unlimited text
-        return new DcmUnlimitedText(key);
-
-      case EVR_SH:  // short string
-        return new DcmShortString(key);
-
-      case EVR_PN:  // person name
-        return new DcmPersonName(key);
-
-        
-      /**
-       * Numerical types
-       **/ 
-      
-      case EVR_SL:  // signed long
-        return new DcmSignedLong(key);
-
-      case EVR_SS:  // signed short
-        return new DcmSignedShort(key);
-
-      case EVR_UL:  // unsigned long
-        return new DcmUnsignedLong(key);
-
-      case EVR_US:  // unsigned short
-        return new DcmUnsignedShort(key);
-
-      case EVR_FL:  // float single-precision
-        return new DcmFloatingPointSingle(key);
-
-      case EVR_FD:  // float double-precision
-        return new DcmFloatingPointDouble(key);
-
-
-      /**
-       * Sequence types, should never occur at this point.
-       **/
-
-      case EVR_SQ:  // sequence of items
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-
-
-      /**
-       * TODO
-       **/
-
-      case EVR_AT:  // attribute tag
-        throw OrthancException(ErrorCode_NotImplemented);
-
-
-      /**
-       * Internal to DCMTK.
-       **/ 
-
-      case EVR_xs:  // SS or US depending on context
-      case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
-      case EVR_na:  // na="not applicable", for data which has no VR
-      case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
-      case EVR_item:  // used internally for items
-      case EVR_metainfo:  // used internally for meta info datasets
-      case EVR_dataset:  // used internally for datasets
-      case EVR_fileFormat:  // used internally for DICOM files
-      case EVR_dicomDir:  // used internally for DICOMDIR objects
-      case EVR_dirRecord:  // used internally for DICOMDIR records
-      case EVR_pixelSQ:  // used internally for pixel sequences in a compressed image
-      case EVR_pixelItem:  // used internally for pixel items in a compressed image
-      case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
-      case EVR_PixelData:  // used internally for uncompressed pixeld data
-      case EVR_OverlayData:  // used internally for overlay data
-      case EVR_UNKNOWN2B:  // used internally for elements with unknown VR with 2-byte length field in explicit VR
-      default:
-        break;
-    }
-
-    throw OrthancException(ErrorCode_InternalError);          
-  }
-
-
-
-  void FromDcmtkBridge::FillElementWithString(DcmElement& element,
-                                              const DicomTag& tag,
-                                              const std::string& utf8Value,
-                                              bool decodeBinaryTags,
-                                              Encoding dicomEncoding)
-  {
-    std::string binary;
-    const std::string* decoded = &utf8Value;
-
-    if (decodeBinaryTags &&
-        boost::starts_with(utf8Value, "data:application/octet-stream;base64,"))
-    {
-      std::string mime;
-      Toolbox::DecodeDataUriScheme(mime, binary, utf8Value);
-      decoded = &binary;
-    }
-    else if (dicomEncoding != Encoding_Utf8)
-    {
-      binary = Toolbox::ConvertFromUtf8(utf8Value, dicomEncoding);
-      decoded = &binary;
-    }
-
-    DcmTag key(tag.GetGroup(), tag.GetElement());
-
-    if (IsBinaryTag(key))
-    {
-      if (element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good())
-      {
-        return;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-
-    bool ok = false;
-    
-    try
-    {
-      switch (key.getEVR())
-      {
-        // http://support.dcmtk.org/docs/dcvr_8h-source.html
-
-        /**
-         * TODO.
-         **/
-
-        case EVR_OB:  // other byte
-        case EVR_OF:  // other float
-        case EVR_OW:  // other word
-        case EVR_AT:  // attribute tag
-          throw OrthancException(ErrorCode_NotImplemented);
-    
-        case EVR_UN:  // unknown value representation
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-
-
-        /**
-         * String types.
-         **/
-      
-        case EVR_DS:  // decimal string
-        case EVR_IS:  // integer string
-        case EVR_AS:  // age string
-        case EVR_DA:  // date string
-        case EVR_DT:  // date time string
-        case EVR_TM:  // time string
-        case EVR_AE:  // application entity title
-        case EVR_CS:  // code string
-        case EVR_SH:  // short string
-        case EVR_LO:  // long string
-        case EVR_ST:  // short text
-        case EVR_LT:  // long text
-        case EVR_UT:  // unlimited text
-        case EVR_PN:  // person name
-        case EVR_UI:  // unique identifier
-        {
-          ok = element.putString(decoded->c_str()).good();
-          break;
-        }
-
-        
-        /**
-         * Numerical types
-         **/ 
-      
-        case EVR_SL:  // signed long
-        {
-          ok = element.putSint32(boost::lexical_cast<Sint32>(*decoded)).good();
-          break;
-        }
-
-        case EVR_SS:  // signed short
-        {
-          ok = element.putSint16(boost::lexical_cast<Sint16>(*decoded)).good();
-          break;
-        }
-
-        case EVR_UL:  // unsigned long
-        {
-          ok = element.putUint32(boost::lexical_cast<Uint32>(*decoded)).good();
-          break;
-        }
-
-        case EVR_US:  // unsigned short
-        {
-          ok = element.putUint16(boost::lexical_cast<Uint16>(*decoded)).good();
-          break;
-        }
-
-        case EVR_FL:  // float single-precision
-        {
-          ok = element.putFloat32(boost::lexical_cast<float>(*decoded)).good();
-          break;
-        }
-
-        case EVR_FD:  // float double-precision
-        {
-          ok = element.putFloat64(boost::lexical_cast<double>(*decoded)).good();
-          break;
-        }
-
-
-        /**
-         * Sequence types, should never occur at this point.
-         **/
-
-        case EVR_SQ:  // sequence of items
-        {
-          ok = false;
-          break;
-        }
-
-
-        /**
-         * Internal to DCMTK.
-         **/ 
-
-        case EVR_ox:  // OB or OW depending on context
-        case EVR_xs:  // SS or US depending on context
-        case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
-        case EVR_na:  // na="not applicable", for data which has no VR
-        case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
-        case EVR_item:  // used internally for items
-        case EVR_metainfo:  // used internally for meta info datasets
-        case EVR_dataset:  // used internally for datasets
-        case EVR_fileFormat:  // used internally for DICOM files
-        case EVR_dicomDir:  // used internally for DICOMDIR objects
-        case EVR_dirRecord:  // used internally for DICOMDIR records
-        case EVR_pixelSQ:  // used internally for pixel sequences in a compressed image
-        case EVR_pixelItem:  // used internally for pixel items in a compressed image
-        case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
-        case EVR_PixelData:  // used internally for uncompressed pixeld data
-        case EVR_OverlayData:  // used internally for overlay data
-        case EVR_UNKNOWN2B:  // used internally for elements with unknown VR with 2-byte length field in explicit VR
-        default:
-          break;
-      }
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-      ok = false;
-    }
-
-    if (!ok)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-
-  DcmElement* FromDcmtkBridge::FromJson(const DicomTag& tag,
-                                        const Json::Value& value,
-                                        bool decodeBinaryTags,
-                                        Encoding dicomEncoding)
-  {
-    std::auto_ptr<DcmElement> element;
-
-    switch (value.type())
-    {
-      case Json::stringValue:
-        element.reset(CreateElementForTag(tag));
-        FillElementWithString(*element, tag, value.asString(), decodeBinaryTags, dicomEncoding);
-        break;
-
-      case Json::arrayValue:
-      {
-        DcmTag key(tag.GetGroup(), tag.GetElement());
-        if (key.getEVR() != EVR_SQ)
-        {
-          throw OrthancException(ErrorCode_BadParameterType);
-        }
-
-        DcmSequenceOfItems* sequence = new DcmSequenceOfItems(key, value.size());
-        element.reset(sequence);
-        
-        for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
-        {
-          std::auto_ptr<DcmItem> item(new DcmItem);
-
-          Json::Value::Members members = value[i].getMemberNames();
-          for (Json::Value::ArrayIndex j = 0; j < members.size(); j++)
-          {
-            item->insert(FromJson(ParseTag(members[j]), value[i][members[j]], decodeBinaryTags, dicomEncoding));
-          }
-
-          sequence->append(item.release());
-        }
-
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_BadParameterType);
-    }
-
-    return element.release();
-  }
-}
--- a/OrthancServer/FromDcmtkBridge.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,136 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ServerEnumerations.h"
-
-#include "../Core/DicomFormat/DicomMap.h"
-
-#include <dcmtk/dcmdata/dcdatset.h>
-#include <json/json.h>
-
-namespace Orthanc
-{
-  class FromDcmtkBridge
-  {
-  public:
-    static void InitializeDictionary();
-
-    static void RegisterDictionaryTag(const DicomTag& tag,
-                                      const DcmEVR& vr,
-                                      const std::string& name,
-                                      unsigned int minMultiplicity,
-                                      unsigned int maxMultiplicity);
-
-    static Encoding DetectEncoding(DcmDataset& dataset);
-
-    static void Convert(DicomMap& target, DcmDataset& dataset);
-
-    static DicomTag Convert(const DcmTag& tag);
-
-    static DicomTag GetTag(const DcmElement& element);
-
-    static bool IsPrivateTag(const DicomTag& tag);
-
-    static bool IsUnknownTag(const DicomTag& tag);
-
-    static DicomValue* ConvertLeafElement(DcmElement& element,
-                                          DicomToJsonFlags flags,
-                                          Encoding encoding);
-
-    static void ToJson(Json::Value& parent,
-                       DcmElement& element,
-                       DicomToJsonFormat format,
-                       DicomToJsonFlags flags,
-                       unsigned int maxStringLength,
-                       Encoding dicomEncoding);
-
-    static void ToJson(Json::Value& target, 
-                       DcmDataset& dataset,
-                       DicomToJsonFormat format,
-                       DicomToJsonFlags flags,
-                       unsigned int maxStringLength);
-
-    static std::string GetName(const DicomTag& tag);
-
-    static DicomTag ParseTag(const char* name);
-
-    static DicomTag ParseTag(const std::string& name)
-    {
-      return ParseTag(name.c_str());
-    }
-
-    static bool HasTag(const DicomMap& fields,
-                       const std::string& tagName)
-    {
-      return fields.HasTag(ParseTag(tagName));
-    }
-
-    static const DicomValue& GetValue(const DicomMap& fields,
-                                      const std::string& tagName)
-    {
-      return fields.GetValue(ParseTag(tagName));
-    }
-
-    static void SetValue(DicomMap& target,
-                         const std::string& tagName,
-                         DicomValue* value)
-    {
-      target.SetValue(ParseTag(tagName), value);
-    }
-
-    static void ToJson(Json::Value& result,
-                       const DicomMap& values,
-                       bool simplify);
-
-    static std::string GenerateUniqueIdentifier(ResourceType level);
-
-    static bool SaveToMemoryBuffer(std::string& buffer,
-                                   DcmDataset& dataSet);
-
-    static ValueRepresentation GetValueRepresentation(const DicomTag& tag);
-
-    static DcmElement* CreateElementForTag(const DicomTag& tag);
-    
-    static void FillElementWithString(DcmElement& element,
-                                      const DicomTag& tag,
-                                      const std::string& utf8alue,  // Encoded using UTF-8
-                                      bool interpretBinaryTags,
-                                      Encoding dicomEncoding);
-
-    static DcmElement* FromJson(const DicomTag& tag,
-                                const Json::Value& element,  // Encoding using UTF-8
-                                bool interpretBinaryTags,
-                                Encoding dicomEncoding);
-  };
-}
--- a/OrthancServer/IDatabaseListener.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/IDatabaseListener.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/IDatabaseWrapper.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/IDatabaseWrapper.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -155,6 +156,12 @@
                                   IdentifierConstraintType type,
                                   const std::string& value) = 0;
 
+    virtual void LookupIdentifierRange(std::list<int64_t>& result,
+                                       ResourceType level,
+                                       const DicomTag& tag,
+                                       const std::string& start,
+                                       const std::string& end) = 0;
+
     virtual bool LookupMetadata(std::string& target,
                                 int64_t id,
                                 MetadataType type) = 0;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/IDicomImageDecoder.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Core/Images/ImageAccessor.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class IDicomImageDecoder : public boost::noncopyable
+  {
+  public:
+    virtual ~IDicomImageDecoder()
+    {
+    }
+
+    virtual ImageAccessor* Decode(const void* dicom,
+                                  size_t size,
+                                  unsigned int frame) = 0;
+  };
+}
--- a/OrthancServer/IServerListener.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/IServerListener.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -39,7 +40,7 @@
 
 namespace Orthanc
 {
-  class IServerListener
+  class IServerListener : public boost::noncopyable
   {
   public:
     virtual ~IServerListener()
@@ -48,8 +49,8 @@
 
     virtual void SignalStoredInstance(const std::string& publicId,
                                       DicomInstanceToStore& instance,
-                                      const Json::Value& simplifiedTags) = 0;                                      
-
+                                      const Json::Value& simplifiedTags) = 0;
+    
     virtual void SignalChange(const ServerIndexChange& change) = 0;
 
     virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance,
--- a/OrthancServer/Internals/CommandDispatcher.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,877 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-
-
-/*=========================================================================
-
-  This file is based on portions of the following project:
-
-  Program: DCMTK 3.6.0
-  Module:  http://dicom.offis.de/dcmtk.php.en
-
-Copyright (C) 1994-2011, OFFIS e.V.
-All rights reserved.
-
-This software and supporting documentation were developed by
-
-  OFFIS e.V.
-  R&D Division Health
-  Escherweg 2
-  26121 Oldenburg, Germany
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
-- Redistributions of source code must retain the above copyright
-  notice, this list of conditions and the following disclaimer.
-
-- Redistributions in binary form must reproduce the above copyright
-  notice, this list of conditions and the following disclaimer in the
-  documentation and/or other materials provided with the distribution.
-
-- Neither the name of OFFIS nor the names of its contributors may be
-  used to endorse or promote products derived from this software
-  without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-=========================================================================*/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "CommandDispatcher.h"
-
-#include "FindScp.h"
-#include "StoreScp.h"
-#include "MoveScp.h"
-#include "../../Core/Toolbox.h"
-#include "../../Core/Logging.h"
-
-#include <dcmtk/dcmnet/dcasccfg.h>      /* for class DcmAssociationConfiguration */
-#include <boost/lexical_cast.hpp>
-
-#define ORTHANC_PROMISCUOUS 1
-
-static OFBool    opt_rejectWithoutImplementationUID = OFFalse;
-
-
-
-#if ORTHANC_PROMISCUOUS == 1
-static
-DUL_PRESENTATIONCONTEXT *
-findPresentationContextID(LST_HEAD * head,
-                          T_ASC_PresentationContextID presentationContextID)
-{
-  DUL_PRESENTATIONCONTEXT *pc;
-  LST_HEAD **l;
-  OFBool found = OFFalse;
-
-  if (head == NULL)
-    return NULL;
-
-  l = &head;
-  if (*l == NULL)
-    return NULL;
-
-  pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Head(l));
-  (void)LST_Position(l, OFstatic_cast(LST_NODE *, pc));
-
-  while (pc && !found) {
-    if (pc->presentationContextID == presentationContextID) {
-      found = OFTrue;
-    } else {
-      pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Next(l));
-    }
-  }
-  return pc;
-}
-
-
-/** accept all presenstation contexts for unknown SOP classes,
- *  i.e. UIDs appearing in the list of abstract syntaxes
- *  where no corresponding name is defined in the UID dictionary.
- *  @param params pointer to association parameters structure
- *  @param transferSyntax transfer syntax to accept
- *  @param acceptedRole SCU/SCP role to accept
- */
-static OFCondition acceptUnknownContextsWithTransferSyntax(
-  T_ASC_Parameters * params,
-  const char* transferSyntax,
-  T_ASC_SC_ROLE acceptedRole)
-{
-  OFCondition cond = EC_Normal;
-  int n, i, k;
-  DUL_PRESENTATIONCONTEXT *dpc;
-  T_ASC_PresentationContext pc;
-  OFBool accepted = OFFalse;
-  OFBool abstractOK = OFFalse;
-
-  n = ASC_countPresentationContexts(params);
-  for (i = 0; i < n; i++)
-  {
-    cond = ASC_getPresentationContext(params, i, &pc);
-    if (cond.bad()) return cond;
-    abstractOK = OFFalse;
-    accepted = OFFalse;
-
-    if (dcmFindNameOfUID(pc.abstractSyntax) == NULL)
-    {
-      abstractOK = OFTrue;
-
-      /* check the transfer syntax */
-      for (k = 0; (k < OFstatic_cast(int, pc.transferSyntaxCount)) && !accepted; k++)
-      {
-        if (strcmp(pc.proposedTransferSyntaxes[k], transferSyntax) == 0)
-        {
-          accepted = OFTrue;
-        }
-      }
-    }
-
-    if (accepted)
-    {
-      cond = ASC_acceptPresentationContext(
-        params, pc.presentationContextID,
-        transferSyntax, acceptedRole);
-      if (cond.bad()) return cond;
-    } else {
-      T_ASC_P_ResultReason reason;
-
-      /* do not refuse if already accepted */
-      dpc = findPresentationContextID(params->DULparams.acceptedPresentationContext,
-                                      pc.presentationContextID);
-      if ((dpc == NULL) || ((dpc != NULL) && (dpc->result != ASC_P_ACCEPTANCE)))
-      {
-
-        if (abstractOK) {
-          reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED;
-        } else {
-          reason = ASC_P_ABSTRACTSYNTAXNOTSUPPORTED;
-        }
-        /*
-         * If previously this presentation context was refused
-         * because of bad transfer syntax let it stay that way.
-         */
-        if ((dpc != NULL) && (dpc->result == ASC_P_TRANSFERSYNTAXESNOTSUPPORTED))
-          reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED;
-
-        cond = ASC_refusePresentationContext(params, pc.presentationContextID, reason);
-        if (cond.bad()) return cond;
-      }
-    }
-  }
-  return EC_Normal;
-}
-
-
-/** accept all presenstation contexts for unknown SOP classes,
- *  i.e. UIDs appearing in the list of abstract syntaxes
- *  where no corresponding name is defined in the UID dictionary.
- *  This method is passed a list of "preferred" transfer syntaxes.
- *  @param params pointer to association parameters structure
- *  @param transferSyntax transfer syntax to accept
- *  @param acceptedRole SCU/SCP role to accept
- */
-static OFCondition acceptUnknownContextsWithPreferredTransferSyntaxes(
-  T_ASC_Parameters * params,
-  const char* transferSyntaxes[], int transferSyntaxCount,
-  T_ASC_SC_ROLE acceptedRole = ASC_SC_ROLE_DEFAULT)
-{
-  OFCondition cond = EC_Normal;
-  /*
-  ** Accept in the order "least wanted" to "most wanted" transfer
-  ** syntax.  Accepting a transfer syntax will override previously
-  ** accepted transfer syntaxes.
-  */
-  for (int i = transferSyntaxCount - 1; i >= 0; i--)
-  {
-    cond = acceptUnknownContextsWithTransferSyntax(params, transferSyntaxes[i], acceptedRole);
-    if (cond.bad()) return cond;
-  }
-  return cond;
-}
-#endif
-
-
-
-namespace Orthanc
-{
-  namespace Internals
-  {
-    /**
-     * EXTRACT OF FILE "dcmdata/libsrc/dcuid.cc" FROM DCMTK 3.6.0
-     * (dcmAllStorageSOPClassUIDs).
-     *
-     * an array of const strings containing all known Storage SOP
-     * Classes that fit into the conventional
-     * PATIENT-STUDY-SERIES-INSTANCE information model,
-     * i.e. everything a Storage SCP might want to store in a PACS.
-     * Special cases such as hanging protocol storage or the Storage
-     * SOP Class are not included in this list.
-     *
-     * THIS LIST CONTAINS ALL STORAGE SOP CLASSES INCLUDING RETIRED
-     * ONES AND IS LARGER THAN 64 ENTRIES.
-     */
-
-    const char* orthancStorageSOPClassUIDs[] =
-    {
-      UID_AmbulatoryECGWaveformStorage,
-      UID_ArterialPulseWaveformStorage,
-      UID_AutorefractionMeasurementsStorage,
-      UID_BasicStructuredDisplayStorage,
-      UID_BasicTextSRStorage,
-      UID_BasicVoiceAudioWaveformStorage,
-      UID_BlendingSoftcopyPresentationStateStorage,
-      UID_BreastTomosynthesisImageStorage,
-      UID_CardiacElectrophysiologyWaveformStorage,
-      UID_ChestCADSRStorage,
-      UID_ColonCADSRStorage,
-      UID_ColorSoftcopyPresentationStateStorage,
-      UID_ComprehensiveSRStorage,
-      UID_ComputedRadiographyImageStorage,
-      UID_CTImageStorage,
-      UID_DeformableSpatialRegistrationStorage,
-      UID_DigitalIntraOralXRayImageStorageForPresentation,
-      UID_DigitalIntraOralXRayImageStorageForProcessing,
-      UID_DigitalMammographyXRayImageStorageForPresentation,
-      UID_DigitalMammographyXRayImageStorageForProcessing,
-      UID_DigitalXRayImageStorageForPresentation,
-      UID_DigitalXRayImageStorageForProcessing,
-      UID_EncapsulatedCDAStorage,
-      UID_EncapsulatedPDFStorage,
-      UID_EnhancedCTImageStorage,
-      UID_EnhancedMRColorImageStorage,
-      UID_EnhancedMRImageStorage,
-      UID_EnhancedPETImageStorage,
-      UID_EnhancedSRStorage,
-      UID_EnhancedUSVolumeStorage,
-      UID_EnhancedXAImageStorage,
-      UID_EnhancedXRFImageStorage,
-      UID_GeneralAudioWaveformStorage,
-      UID_GeneralECGWaveformStorage,
-      UID_GenericImplantTemplateStorage,
-      UID_GrayscaleSoftcopyPresentationStateStorage,
-      UID_HemodynamicWaveformStorage,
-      UID_ImplantAssemblyTemplateStorage,
-      UID_ImplantationPlanSRDocumentStorage,
-      UID_ImplantTemplateGroupStorage,
-      UID_IntraocularLensCalculationsStorage,
-      UID_KeratometryMeasurementsStorage,
-      UID_KeyObjectSelectionDocumentStorage,
-      UID_LensometryMeasurementsStorage,
-      UID_MacularGridThicknessAndVolumeReportStorage,
-      UID_MammographyCADSRStorage,
-      UID_MRImageStorage,
-      UID_MRSpectroscopyStorage,
-      UID_MultiframeGrayscaleByteSecondaryCaptureImageStorage,
-      UID_MultiframeGrayscaleWordSecondaryCaptureImageStorage,
-      UID_MultiframeSingleBitSecondaryCaptureImageStorage,
-      UID_MultiframeTrueColorSecondaryCaptureImageStorage,
-      UID_NuclearMedicineImageStorage,
-      UID_OphthalmicAxialMeasurementsStorage,
-      UID_OphthalmicPhotography16BitImageStorage,
-      UID_OphthalmicPhotography8BitImageStorage,
-      UID_OphthalmicTomographyImageStorage,
-      UID_OphthalmicVisualFieldStaticPerimetryMeasurementsStorage,
-      UID_PositronEmissionTomographyImageStorage,
-      UID_ProcedureLogStorage,
-      UID_PseudoColorSoftcopyPresentationStateStorage,
-      UID_RawDataStorage,
-      UID_RealWorldValueMappingStorage,
-      UID_RespiratoryWaveformStorage,
-      UID_RTBeamsTreatmentRecordStorage,
-      UID_RTBrachyTreatmentRecordStorage,
-      UID_RTDoseStorage,
-      UID_RTImageStorage,
-      UID_RTIonBeamsTreatmentRecordStorage,
-      UID_RTIonPlanStorage,
-      UID_RTPlanStorage,
-      UID_RTStructureSetStorage,
-      UID_RTTreatmentSummaryRecordStorage,
-      UID_SecondaryCaptureImageStorage,
-      UID_SegmentationStorage,
-      UID_SpatialFiducialsStorage,
-      UID_SpatialRegistrationStorage,
-      UID_SpectaclePrescriptionReportStorage,
-      UID_StereometricRelationshipStorage,
-      UID_SubjectiveRefractionMeasurementsStorage,
-      UID_SurfaceSegmentationStorage,
-      UID_TwelveLeadECGWaveformStorage,
-      UID_UltrasoundImageStorage,
-      UID_UltrasoundMultiframeImageStorage,
-      UID_VideoEndoscopicImageStorage,
-      UID_VideoMicroscopicImageStorage,
-      UID_VideoPhotographicImageStorage,
-      UID_VisualAcuityMeasurementsStorage,
-      UID_VLEndoscopicImageStorage,
-      UID_VLMicroscopicImageStorage,
-      UID_VLPhotographicImageStorage,
-      UID_VLSlideCoordinatesMicroscopicImageStorage,
-      UID_VLWholeSlideMicroscopyImageStorage,
-      UID_XAXRFGrayscaleSoftcopyPresentationStateStorage,
-      UID_XRay3DAngiographicImageStorage,
-      UID_XRay3DCraniofacialImageStorage,
-      UID_XRayAngiographicImageStorage,
-      UID_XRayRadiationDoseSRStorage,
-      UID_XRayRadiofluoroscopicImageStorage,
-      // retired
-      UID_RETIRED_HardcopyColorImageStorage,
-      UID_RETIRED_HardcopyGrayscaleImageStorage,
-      UID_RETIRED_NuclearMedicineImageStorage,
-      UID_RETIRED_StandaloneCurveStorage,
-      UID_RETIRED_StandaloneModalityLUTStorage,
-      UID_RETIRED_StandaloneOverlayStorage,
-      UID_RETIRED_StandalonePETCurveStorage,
-      UID_RETIRED_StandaloneVOILUTStorage,
-      UID_RETIRED_StoredPrintStorage,
-      UID_RETIRED_UltrasoundImageStorage,
-      UID_RETIRED_UltrasoundMultiframeImageStorage,
-      UID_RETIRED_VLImageStorage,
-      UID_RETIRED_VLMultiFrameImageStorage,
-      UID_RETIRED_XRayAngiographicBiPlaneImageStorage,
-      // draft
-      UID_DRAFT_SRAudioStorage,
-      UID_DRAFT_SRComprehensiveStorage,
-      UID_DRAFT_SRDetailStorage,
-      UID_DRAFT_SRTextStorage,
-      UID_DRAFT_WaveformStorage,
-      UID_DRAFT_RTBeamsDeliveryInstructionStorage,
-      NULL
-    };
-
-    const int orthancStorageSOPClassUIDsCount = (sizeof(orthancStorageSOPClassUIDs) / sizeof(const char*)) - 1;
-
-
-
-    OFCondition AssociationCleanup(T_ASC_Association *assoc)
-    {
-      OFString temp_str;
-      OFCondition cond = ASC_dropSCPAssociation(assoc);
-      if (cond.bad())
-      {
-        LOG(FATAL) << cond.text();
-        return cond;
-      }
-
-      cond = ASC_destroyAssociation(&assoc);
-      if (cond.bad())
-      {
-        LOG(FATAL) << cond.text();
-        return cond;
-      }
-
-      return cond;
-    }
-
-
-
-    CommandDispatcher* AcceptAssociation(const DicomServer& server, T_ASC_Network *net)
-    {
-      DcmAssociationConfiguration asccfg;
-      char buf[BUFSIZ];
-      T_ASC_Association *assoc;
-      OFCondition cond;
-      OFString sprofile;
-      OFString temp_str;
-
-      std::vector<const char*> knownAbstractSyntaxes;
-
-      // For C-STORE
-      if (server.HasStoreRequestHandlerFactory())
-      {
-        knownAbstractSyntaxes.push_back(UID_VerificationSOPClass);
-      }
-
-      // For C-FIND
-      if (server.HasFindRequestHandlerFactory())
-      {
-        knownAbstractSyntaxes.push_back(UID_FINDPatientRootQueryRetrieveInformationModel);
-        knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel);
-      }
-
-      // For C-MOVE
-      if (server.HasMoveRequestHandlerFactory())
-      {
-        knownAbstractSyntaxes.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel);
-        knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel);
-      }
-
-      cond = ASC_receiveAssociation(net, &assoc, 
-                                    /*opt_maxPDU*/ ASC_DEFAULTMAXPDU, 
-                                    NULL, NULL,
-                                    /*opt_secureConnection*/ OFFalse,
-                                    DUL_NOBLOCK, 1);
-
-      if (cond == DUL_NOASSOCIATIONREQUEST)
-      {
-        // Timeout
-        AssociationCleanup(assoc);
-        return NULL;
-      }
-
-      // if some kind of error occured, take care of it
-      if (cond.bad())
-      {
-        LOG(ERROR) << "Receiving Association failed: " << cond.text();
-        // no matter what kind of error occurred, we need to do a cleanup
-        AssociationCleanup(assoc);
-        return NULL;
-      }
-
-      // Retrieve the AET and the IP address of the remote modality
-      std::string callingAet;
-      std::string callingIp;
-      std::string calledAet;
-  
-      {
-        DIC_AE callingAet_C;
-        DIC_AE calledAet_C;
-        DIC_AE callingIp_C;
-        DIC_AE calledIP_C;
-        if (ASC_getAPTitles(assoc->params, callingAet_C, calledAet_C, NULL).bad() ||
-            ASC_getPresentationAddresses(assoc->params, callingIp_C, calledIP_C).bad())
-        {
-          T_ASC_RejectParameters rej =
-            {
-              ASC_RESULT_REJECTEDPERMANENT,
-              ASC_SOURCE_SERVICEUSER,
-              ASC_REASON_SU_NOREASON
-            };
-          ASC_rejectAssociation(assoc, &rej);
-          AssociationCleanup(assoc);
-          return NULL;
-        }
-
-        callingIp = std::string(/*OFSTRING_GUARD*/(callingIp_C));
-        callingAet = std::string(/*OFSTRING_GUARD*/(callingAet_C));
-        calledAet = (/*OFSTRING_GUARD*/(calledAet_C));
-      }
-
-      LOG(INFO) << "Association Received from AET " << callingAet 
-                << " on IP " << callingIp;
-
-
-      std::vector<const char*> transferSyntaxes;
-
-      // This is the list of the transfer syntaxes that were supported up to Orthanc 0.7.1
-      transferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
-      transferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
-      transferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
-
-      // New transfer syntaxes supported since Orthanc 0.7.2
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Deflated))
-      {
-        transferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); 
-      }
-
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpeg))
-      {
-        transferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax);
-      }
-
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpeg2000))
-      {
-        transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
-        transferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
-      }
-
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_JpegLossless))
-      {
-        transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
-        transferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax);
-        transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax);
-      }
-
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpip))
-      {
-        transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax);
-        transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax);
-      }
-
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Mpeg2))
-      {
-        transferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax);
-        transferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax);
-      }
-
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Rle))
-      {
-        transferSyntaxes.push_back(UID_RLELosslessTransferSyntax);
-      }
-
-      /* accept the Verification SOP Class if presented */
-      cond = ASC_acceptContextsWithPreferredTransferSyntaxes(assoc->params, &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(), &transferSyntaxes[0], transferSyntaxes.size());
-      if (cond.bad())
-      {
-        LOG(INFO) << cond.text();
-        AssociationCleanup(assoc);
-        return NULL;
-      }
-
-      /* the array of Storage SOP Class UIDs comes from dcuid.h */
-      cond = ASC_acceptContextsWithPreferredTransferSyntaxes(assoc->params, orthancStorageSOPClassUIDs, orthancStorageSOPClassUIDsCount, &transferSyntaxes[0], transferSyntaxes.size());
-      if (cond.bad())
-      {
-        LOG(INFO) << cond.text();
-        AssociationCleanup(assoc);
-        return NULL;
-      }
-
-#if ORTHANC_PROMISCUOUS == 1
-      /* accept everything not known not to be a storage SOP class */
-      cond = acceptUnknownContextsWithPreferredTransferSyntaxes(
-        assoc->params, &transferSyntaxes[0], transferSyntaxes.size());
-      if (cond.bad())
-      {
-        LOG(INFO) << cond.text();
-        AssociationCleanup(assoc);
-        return NULL;
-      }
-#endif
-
-      /* set our app title */
-      ASC_setAPTitles(assoc->params, NULL, NULL, server.GetApplicationEntityTitle().c_str());
-
-      /* acknowledge or reject this association */
-      cond = ASC_getApplicationContextName(assoc->params, buf);
-      if ((cond.bad()) || strcmp(buf, UID_StandardApplicationContext) != 0)
-      {
-        /* reject: the application context name is not supported */
-        T_ASC_RejectParameters rej =
-          {
-            ASC_RESULT_REJECTEDPERMANENT,
-            ASC_SOURCE_SERVICEUSER,
-            ASC_REASON_SU_APPCONTEXTNAMENOTSUPPORTED
-          };
-
-        LOG(INFO) << "Association Rejected: Bad Application Context Name: " << buf;
-        cond = ASC_rejectAssociation(assoc, &rej);
-        if (cond.bad())
-        {
-          LOG(INFO) << cond.text();
-        }
-        AssociationCleanup(assoc);
-        return NULL;
-      }
-
-      /* check the AETs */
-      if (!server.IsMyAETitle(calledAet))
-      {
-        LOG(WARNING) << "Rejected association, because of a bad called AET in the request (" << calledAet << ")";
-        T_ASC_RejectParameters rej =
-          {
-            ASC_RESULT_REJECTEDPERMANENT,
-            ASC_SOURCE_SERVICEUSER,
-            ASC_REASON_SU_CALLEDAETITLENOTRECOGNIZED
-          };
-        ASC_rejectAssociation(assoc, &rej);
-        AssociationCleanup(assoc);
-        return NULL;
-      }
-
-      if (server.HasApplicationEntityFilter() &&
-          !server.GetApplicationEntityFilter().IsAllowedConnection(callingIp, callingAet))
-      {
-        LOG(WARNING) << "Rejected association for remote AET " << callingAet << " on IP " << callingIp;
-        T_ASC_RejectParameters rej =
-          {
-            ASC_RESULT_REJECTEDPERMANENT,
-            ASC_SOURCE_SERVICEUSER,
-            ASC_REASON_SU_CALLINGAETITLENOTRECOGNIZED
-          };
-        ASC_rejectAssociation(assoc, &rej);
-        AssociationCleanup(assoc);
-        return NULL;
-      }
-
-      if (opt_rejectWithoutImplementationUID && 
-          strlen(assoc->params->theirImplementationClassUID) == 0)
-      {
-        /* reject: the no implementation Class UID provided */
-        T_ASC_RejectParameters rej =
-          {
-            ASC_RESULT_REJECTEDPERMANENT,
-            ASC_SOURCE_SERVICEUSER,
-            ASC_REASON_SU_NOREASON
-          };
-
-        LOG(INFO) << "Association Rejected: No Implementation Class UID provided";
-        cond = ASC_rejectAssociation(assoc, &rej);
-        if (cond.bad())
-        {
-          LOG(INFO) << cond.text();
-        }
-        AssociationCleanup(assoc);
-        return NULL;
-      }
-
-      {
-        cond = ASC_acknowledgeAssociation(assoc);
-        if (cond.bad())
-        {
-          LOG(ERROR) << cond.text();
-          AssociationCleanup(assoc);
-          return NULL;
-        }
-        LOG(INFO) << "Association Acknowledged (Max Send PDV: " << assoc->sendPDVLength << ")";
-        if (ASC_countAcceptedPresentationContexts(assoc->params) == 0)
-          LOG(INFO) << "    (but no valid presentation contexts)";
-      }
-
-      IApplicationEntityFilter* filter = server.HasApplicationEntityFilter() ? &server.GetApplicationEntityFilter() : NULL;
-      return new CommandDispatcher(server, assoc, callingIp, callingAet, filter);
-    }
-
-    bool CommandDispatcher::Step()
-    /*
-     * This function receives DIMSE commmands over the network connection
-     * and handles these commands correspondingly. Note that in case of
-     * storscp only C-ECHO-RQ and C-STORE-RQ commands can be processed.
-     */
-    {
-      bool finished = false;
-
-      // receive a DIMSE command over the network, with a timeout of 1 second
-      DcmDataset *statusDetail = NULL;
-      T_ASC_PresentationContextID presID = 0;
-      T_DIMSE_Message msg;
-
-      OFCondition cond = DIMSE_receiveCommand(assoc_, DIMSE_NONBLOCKING, 1, &presID, &msg, &statusDetail);
-      elapsedTimeSinceLastCommand_++;
-    
-      // if the command which was received has extra status
-      // detail information, dump this information
-      if (statusDetail != NULL)
-      {
-        //LOG4CPP_WARN(Internals::GetLogger(), "Status Detail:" << OFendl << DcmObject::PrintHelper(*statusDetail));
-        delete statusDetail;
-      }
-
-      if (cond == DIMSE_OUTOFRESOURCES)
-      {
-        finished = true;
-      }
-      else if (cond == DIMSE_NODATAAVAILABLE)
-      {
-        // Timeout due to DIMSE_NONBLOCKING
-        if (clientTimeout_ != 0 && 
-            elapsedTimeSinceLastCommand_ >= clientTimeout_)
-        {
-          // This timeout is actually a client timeout
-          finished = true;
-        }
-      }
-      else if (cond == EC_Normal)
-      {
-        // Reset the client timeout counter
-        elapsedTimeSinceLastCommand_ = 0;
-
-        // Convert the type of request to Orthanc's internal type
-        bool supported = false;
-        DicomRequestType request;
-        switch (msg.CommandField)
-        {
-          case DIMSE_C_ECHO_RQ:
-            request = DicomRequestType_Echo;
-            supported = true;
-            break;
-
-          case DIMSE_C_STORE_RQ:
-            request = DicomRequestType_Store;
-            supported = true;
-            break;
-
-          case DIMSE_C_MOVE_RQ:
-            request = DicomRequestType_Move;
-            supported = true;
-            break;
-
-          case DIMSE_C_FIND_RQ:
-            request = DicomRequestType_Find;
-            supported = true;
-            break;
-
-          default:
-            // we cannot handle this kind of message
-            cond = DIMSE_BADCOMMANDTYPE;
-            LOG(ERROR) << "cannot handle command: 0x" << std::hex << msg.CommandField;
-            break;
-        }
-
-
-        // Check whether this request is allowed by the security filter
-        if (supported && 
-            request != DicomRequestType_Echo &&  // Always allow incoming ECHO requests
-            filter_ != NULL &&
-            !filter_->IsAllowedRequest(remoteIp_, remoteAet_, request))
-        {
-          LOG(ERROR) << EnumerationToString(request) 
-                     << " requests are disallowed for the AET \"" 
-                     << remoteAet_ << "\"";
-          cond = DIMSE_ILLEGALASSOCIATION;
-          supported = false;
-          finished = true;
-        }
-
-        // in case we received a supported message, process this command
-        if (supported)
-        {
-          // If anything goes wrong, there will be a "BADCOMMANDTYPE" answer
-          cond = DIMSE_BADCOMMANDTYPE;
-
-          switch (request)
-          {
-            case DicomRequestType_Echo:
-              cond = EchoScp(assoc_, &msg, presID);
-              break;
-
-            case DicomRequestType_Store:
-              if (server_.HasStoreRequestHandlerFactory()) // Should always be true
-              {
-                std::auto_ptr<IStoreRequestHandler> handler
-                  (server_.GetStoreRequestHandlerFactory().ConstructStoreRequestHandler());
-                cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_);
-              }
-              break;
-
-            case DicomRequestType_Move:
-              if (server_.HasMoveRequestHandlerFactory()) // Should always be true
-              {
-                std::auto_ptr<IMoveRequestHandler> handler
-                  (server_.GetMoveRequestHandlerFactory().ConstructMoveRequestHandler());
-                cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_);
-              }
-              break;
-
-            case DicomRequestType_Find:
-              if (server_.HasFindRequestHandlerFactory()) // Should always be true
-              {
-                std::auto_ptr<IFindRequestHandler> handler
-                  (server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler());
-                cond = Internals::findScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_);
-              }
-              break;
-
-            default:
-              // Should never happen
-              break;
-          }
-        }
-      }
-      else
-      {
-        // Bad status, which indicates the closing of the connection by
-        // the peer or a network error
-        finished = true;
-
-        LOG(INFO) << cond.text();
-      }
-    
-      if (finished)
-      {
-        if (cond == DUL_PEERREQUESTEDRELEASE)
-        {
-          LOG(INFO) << "Association Release";
-          ASC_acknowledgeRelease(assoc_);
-        }
-        else if (cond == DUL_PEERABORTEDASSOCIATION)
-        {
-          LOG(INFO) << "Association Aborted";
-        }
-        else
-        {
-          OFString temp_str;
-          LOG(ERROR) << "DIMSE failure (aborting association): " << cond.text();
-          /* some kind of error so abort the association */
-          ASC_abortAssociation(assoc_);
-        }
-      }
-
-      return !finished;
-    }
-
-
-    OFCondition EchoScp(T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID)
-    {
-      OFString temp_str;
-      LOG(INFO) << "Received Echo Request";
-      //LOG(DEBUG) << DIMSE_dumpMessage(temp_str, msg->msg.CEchoRQ, DIMSE_INCOMING, NULL, presID));
-
-      /* the echo succeeded !! */
-      OFCondition cond = DIMSE_sendEchoResponse(assoc, presID, &msg->msg.CEchoRQ, STATUS_Success, NULL);
-      if (cond.bad())
-      {
-        LOG(ERROR) << "Echo SCP Failed: " << cond.text();
-      }
-      return cond;
-    }
-  }
-}
--- a/OrthancServer/Internals/CommandDispatcher.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../DicomProtocol/DicomServer.h"
-#include "../../Core/MultiThreading/IRunnableBySteps.h"
-
-#include <dcmtk/dcmnet/dimse.h>
-
-namespace Orthanc
-{
-  namespace Internals
-  {
-    OFCondition AssociationCleanup(T_ASC_Association *assoc);
-
-    class CommandDispatcher : public IRunnableBySteps
-    {
-    private:
-      uint32_t clientTimeout_;
-      uint32_t elapsedTimeSinceLastCommand_;
-      const DicomServer& server_;
-      T_ASC_Association* assoc_;
-      std::string remoteIp_;
-      std::string remoteAet_;
-      IApplicationEntityFilter* filter_;
-
-    public:
-      CommandDispatcher(const DicomServer& server,
-                        T_ASC_Association* assoc,
-                        const std::string& remoteIp,
-                        const std::string& remoteAet,
-                        IApplicationEntityFilter* filter) :
-        server_(server),
-        assoc_(assoc),
-        remoteIp_(remoteIp),
-        remoteAet_(remoteAet),
-        filter_(filter)
-      {
-        clientTimeout_ = server.GetClientTimeout();
-        elapsedTimeSinceLastCommand_ = 0;
-      }
-
-      virtual ~CommandDispatcher()
-      {
-        AssociationCleanup(assoc_);
-      }
-
-      virtual bool Step();
-    };
-
-    OFCondition EchoScp(T_ASC_Association * assoc, 
-                        T_DIMSE_Message * msg, 
-                        T_ASC_PresentationContextID presID);
-
-    CommandDispatcher* AcceptAssociation(const DicomServer& server, 
-                                         T_ASC_Network *net);
-  }
-}
--- a/OrthancServer/Internals/DicomImageDecoder.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,698 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "DicomImageDecoder.h"
-
-
-/*=========================================================================
-
-  This file is based on portions of the following project
-  (cf. function "DecodePsmctRle1()"):
-
-  Program: GDCM (Grassroots DICOM). A DICOM library
-  Module:  http://gdcm.sourceforge.net/Copyright.html
-
-  Copyright (c) 2006-2011 Mathieu Malaterre
-  Copyright (c) 1993-2005 CREATIS
-  (CREATIS = Centre de Recherche et d'Applications en Traitement de l'Image)
-  All rights reserved.
-
-  Redistribution and use in source and binary forms, with or without
-  modification, are permitted provided that the following conditions are met:
-
-  * Redistributions of source code must retain the above copyright notice,
-  this list of conditions and the following disclaimer.
-
-  * Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-  * Neither name of Mathieu Malaterre, or CREATIS, nor the names of any
-  contributors (CNRS, INSERM, UCB, Universite Lyon I), may be used to
-  endorse or promote products derived from this software without specific
-  prior written permission.
-
-  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
-  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-  ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
-  ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-  =========================================================================*/
-
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "DicomImageDecoder.h"
-
-#include "../../Core/Logging.h"
-#include "../../Core/OrthancException.h"
-#include "../../Core/Images/ImageProcessing.h"
-#include "../../Core/Images/PngWriter.h"  // TODO REMOVE THIS
-#include "../../Core/DicomFormat/DicomIntegerPixelAccessor.h"
-#include "../ToDcmtkBridge.h"
-#include "../FromDcmtkBridge.h"
-
-#include <boost/lexical_cast.hpp>
-
-#if ORTHANC_JPEG_LOSSLESS_ENABLED == 1
-#include <dcmtk/dcmjpls/djcodecd.h>
-#include <dcmtk/dcmjpls/djcparam.h>
-#include <dcmtk/dcmjpeg/djrplol.h>
-#endif
-
-
-namespace Orthanc
-{
-  static const DicomTag DICOM_TAG_CONTENT(0x07a1, 0x100a);
-  static const DicomTag DICOM_TAG_COMPRESSION_TYPE(0x07a1, 0x1011);
-
-
-  static bool IsJpegLossless(const DcmDataset& dataset)
-  {
-    // http://support.dcmtk.org/docs/dcxfer_8h-source.html
-    return (dataset.getOriginalXfer() == EXS_JPEGLSLossless ||
-            dataset.getOriginalXfer() == EXS_JPEGLSLossy);
-  }
-
-
-  static bool IsPsmctRle1(DcmDataset& dataset)
-  {
-    DcmElement* e;
-    char* c;
-
-    // Check whether the DICOM instance contains an image encoded with
-    // the PMSCT_RLE1 scheme.
-    if (!dataset.findAndGetElement(ToDcmtkBridge::Convert(DICOM_TAG_COMPRESSION_TYPE), e).good() ||
-        e == NULL ||
-        !e->isaString() ||
-        !e->getString(c).good() ||
-        c == NULL ||
-        strcmp("PMSCT_RLE1", c))
-    {
-      return false;
-    }
-    else
-    {
-      return true;
-    }
-  }
-
-
-  static bool DecodePsmctRle1(std::string& output,
-                              DcmDataset& dataset)
-  {
-    // Check whether the DICOM instance contains an image encoded with
-    // the PMSCT_RLE1 scheme.
-    if (!IsPsmctRle1(dataset))
-    {
-      return false;
-    }
-
-    // OK, this is a custom RLE encoding from Philips. Get the pixel
-    // data from the appropriate private DICOM tag.
-    Uint8* pixData = NULL;
-    DcmElement* e;
-    if (!dataset.findAndGetElement(ToDcmtkBridge::Convert(DICOM_TAG_CONTENT), e).good() ||
-        e == NULL ||
-        e->getUint8Array(pixData) != EC_Normal)
-    {
-      return false;
-    }    
-
-    // The "unsigned" below IS VERY IMPORTANT
-    const uint8_t* inbuffer = reinterpret_cast<const uint8_t*>(pixData);
-    const size_t length = e->getLength();
-
-    /**
-     * The code below is an adaptation of a sample code for GDCM by
-     * Mathieu Malaterre (under a BSD license).
-     * http://gdcm.sourceforge.net/html/rle2img_8cxx-example.html
-     **/
-
-    // RLE pass
-    std::vector<uint8_t> temp;
-    temp.reserve(length);
-    for (size_t i = 0; i < length; i++)
-    {
-      if (inbuffer[i] == 0xa5)
-      {
-        temp.push_back(inbuffer[i+2]);
-        for (uint8_t repeat = inbuffer[i + 1]; repeat != 0; repeat--)
-        {
-          temp.push_back(inbuffer[i+2]);
-        }
-        i += 2;
-      }
-      else
-      {
-        temp.push_back(inbuffer[i]);
-      }
-    }
-
-    // Delta encoding pass
-    uint16_t delta = 0;
-    output.clear();
-    output.reserve(temp.size());
-    for (size_t i = 0; i < temp.size(); i++)
-    {
-      uint16_t value;
-
-      if (temp[i] == 0x5a)
-      {
-        uint16_t v1 = temp[i + 1];
-        uint16_t v2 = temp[i + 2];
-        value = (v2 << 8) + v1;
-        i += 2;
-      }
-      else
-      {
-        value = delta + (int8_t) temp[i];
-      }
-
-      output.push_back(value & 0xff);
-      output.push_back(value >> 8);
-      delta = value;
-    }
-
-    if (output.size() % 2)
-    {
-      output.resize(output.size() - 1);
-    }
-
-    return true;
-  }
-
-
-  class DicomImageDecoder::ImageSource
-  {
-  private:
-    std::string psmct_;
-    std::auto_ptr<DicomIntegerPixelAccessor> slowAccessor_;
-
-  public:
-    void Setup(DcmDataset& dataset,
-               unsigned int frame)
-    {
-      psmct_.clear();
-      slowAccessor_.reset(NULL);
-
-      // See also: http://support.dcmtk.org/wiki/dcmtk/howto/accessing-compressed-data
-
-      DicomMap m;
-      FromDcmtkBridge::Convert(m, dataset);
-
-      /**
-       * Create an accessor to the raw values of the DICOM image.
-       **/
-
-      DcmElement* e;
-      if (dataset.findAndGetElement(ToDcmtkBridge::Convert(DICOM_TAG_PIXEL_DATA), e).good() &&
-          e != NULL)
-      {
-        Uint8* pixData = NULL;
-        if (e->getUint8Array(pixData) == EC_Normal)
-        {    
-          slowAccessor_.reset(new DicomIntegerPixelAccessor(m, pixData, e->getLength()));
-        }
-      }
-      else if (DecodePsmctRle1(psmct_, dataset))
-      {
-        LOG(INFO) << "The PMSCT_RLE1 decoding has succeeded";
-        Uint8* pixData = NULL;
-        if (psmct_.size() > 0)
-        {
-          pixData = reinterpret_cast<Uint8*>(&psmct_[0]);
-        }
-
-        slowAccessor_.reset(new DicomIntegerPixelAccessor(m, pixData, psmct_.size()));
-      }
-    
-      if (slowAccessor_.get() == NULL)
-      {
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      slowAccessor_->SetCurrentFrame(frame);
-    }
-
-    unsigned int GetWidth() const
-    {
-      assert(slowAccessor_.get() != NULL);
-      return slowAccessor_->GetInformation().GetWidth();
-    }
-
-    unsigned int GetHeight() const
-    {
-      assert(slowAccessor_.get() != NULL);
-      return slowAccessor_->GetInformation().GetHeight();
-    }
-
-    unsigned int GetChannelCount() const
-    {
-      assert(slowAccessor_.get() != NULL);
-      return slowAccessor_->GetInformation().GetChannelCount();
-    }
-
-    const DicomIntegerPixelAccessor& GetAccessor() const
-    {
-      assert(slowAccessor_.get() != NULL);
-      return *slowAccessor_;
-    }
-
-    unsigned int GetSize() const
-    {
-      assert(slowAccessor_.get() != NULL);
-      return slowAccessor_->GetSize();
-    }
-  };
-
-
-  void DicomImageDecoder::SetupImageBuffer(ImageBuffer& target,
-                                           DcmDataset& dataset)
-  {
-    DicomMap m;
-    FromDcmtkBridge::Convert(m, dataset);
-
-    DicomImageInformation info(m);
-    PixelFormat format;
-    
-    if (!info.ExtractPixelFormat(format))
-    {
-      LOG(WARNING) << "Unsupported DICOM image: " << info.GetBitsStored() 
-                   << "bpp, " << info.GetChannelCount() << " channels, " 
-                   << (info.IsSigned() ? "signed" : "unsigned")
-                   << (info.IsPlanar() ? ", planar, " : ", non-planar, ")
-                   << EnumerationToString(info.GetPhotometricInterpretation())
-                   << " photometric interpretation";
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-
-    target.SetHeight(info.GetHeight());
-    target.SetWidth(info.GetWidth());
-    target.SetFormat(format);
-  }
-
-
-  bool DicomImageDecoder::IsUncompressedImage(const DcmDataset& dataset)
-  {
-    // http://support.dcmtk.org/docs/dcxfer_8h-source.html
-    return (dataset.getOriginalXfer() == EXS_Unknown ||
-            dataset.getOriginalXfer() == EXS_LittleEndianImplicit ||
-            dataset.getOriginalXfer() == EXS_BigEndianImplicit ||
-            dataset.getOriginalXfer() == EXS_LittleEndianExplicit ||
-            dataset.getOriginalXfer() == EXS_BigEndianExplicit);
-  }
-
-
-  template <typename PixelType>
-  static void CopyPixels(ImageAccessor& target,
-                         const DicomIntegerPixelAccessor& source)
-  {
-    const PixelType minValue = std::numeric_limits<PixelType>::min();
-    const PixelType maxValue = std::numeric_limits<PixelType>::max();
-
-    for (unsigned int y = 0; y < source.GetInformation().GetHeight(); y++)
-    {
-      PixelType* pixel = reinterpret_cast<PixelType*>(target.GetRow(y));
-      for (unsigned int x = 0; x < source.GetInformation().GetWidth(); x++)
-      {
-        for (unsigned int c = 0; c < source.GetInformation().GetChannelCount(); c++, pixel++)
-        {
-          int32_t v = source.GetValue(x, y, c);
-          if (v < static_cast<int32_t>(minValue))
-          {
-            *pixel = minValue;
-          }
-          else if (v > static_cast<int32_t>(maxValue))
-          {
-            *pixel = maxValue;
-          }
-          else
-          {
-            *pixel = static_cast<PixelType>(v);
-          }
-        }
-      }
-    }
-  }
-
-
-  void DicomImageDecoder::DecodeUncompressedImage(ImageBuffer& target,
-                                                  DcmDataset& dataset,
-                                                  unsigned int frame)
-  {
-    if (!IsUncompressedImage(dataset))
-    {
-      throw OrthancException(ErrorCode_BadParameterType);
-    }
-
-    DecodeUncompressedImageInternal(target, dataset, frame);
-  }
-
-
-  void DicomImageDecoder::DecodeUncompressedImageInternal(ImageBuffer& target,
-                                                          DcmDataset& dataset,
-                                                          unsigned int frame)
-  {
-    ImageSource source;
-    source.Setup(dataset, frame);
-
-
-    /**
-     * Resize the target image.
-     **/
-
-    SetupImageBuffer(target, dataset);
-
-    if (source.GetWidth() != target.GetWidth() ||
-        source.GetHeight() != target.GetHeight())
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-
-    /**
-     * If the format of the DICOM buffer is natively supported, use a
-     * direct access to copy its values.
-     **/
-
-    ImageAccessor targetAccessor(target.GetAccessor());
-    const DicomImageInformation& info = source.GetAccessor().GetInformation();
-
-    bool fastVersionSuccess = false;
-    PixelFormat sourceFormat;
-    if (!info.IsPlanar() &&
-        info.ExtractPixelFormat(sourceFormat))
-    {
-      try
-      {
-        size_t frameSize = info.GetHeight() * info.GetWidth() * GetBytesPerPixel(sourceFormat);
-        if ((frame + 1) * frameSize <= source.GetSize())
-        {
-          const uint8_t* buffer = reinterpret_cast<const uint8_t*>(source.GetAccessor().GetPixelData());
-
-          ImageAccessor sourceImage;
-          sourceImage.AssignReadOnly(sourceFormat, 
-                                     info.GetWidth(), 
-                                     info.GetHeight(),
-                                     info.GetWidth() * GetBytesPerPixel(sourceFormat),
-                                     buffer + frame * frameSize);
-
-          ImageProcessing::Convert(targetAccessor, sourceImage);
-          ImageProcessing::ShiftRight(targetAccessor, info.GetShift());
-          fastVersionSuccess = true;
-        }
-      }
-      catch (OrthancException&)
-      {
-        // Unsupported conversion, use the slow version
-      }
-    }
-
-    /**
-     * Slow version : loop over the DICOM buffer, storing its value
-     * into the target image.
-     **/
-
-    if (!fastVersionSuccess)
-    {
-      switch (target.GetFormat())
-      {
-        case PixelFormat_RGB24:
-        case PixelFormat_RGBA32:
-        case PixelFormat_Grayscale8:
-          CopyPixels<uint8_t>(targetAccessor, source.GetAccessor());
-          break;
-        
-        case PixelFormat_Grayscale16:
-          CopyPixels<uint16_t>(targetAccessor, source.GetAccessor());
-          break;
-
-        case PixelFormat_SignedGrayscale16:
-          CopyPixels<int16_t>(targetAccessor, source.GetAccessor());
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-  }
-
-
-#if ORTHANC_JPEG_LOSSLESS_ENABLED == 1
-  void DicomImageDecoder::DecodeJpegLossless(ImageBuffer& target,
-                                             DcmDataset& dataset,
-                                             unsigned int frame)
-  {
-    if (!IsJpegLossless(dataset))
-    {
-      throw OrthancException(ErrorCode_BadParameterType);
-    }
-
-    DcmElement *element = NULL;
-    if (!dataset.findAndGetElement(ToDcmtkBridge::Convert(DICOM_TAG_PIXEL_DATA), element).good())
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    DcmPixelData& pixelData = dynamic_cast<DcmPixelData&>(*element);
-    DcmPixelSequence* pixelSequence = NULL;
-    if (!pixelData.getEncapsulatedRepresentation
-        (dataset.getOriginalXfer(), NULL, pixelSequence).good())
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    SetupImageBuffer(target, dataset);
-
-    ImageAccessor targetAccessor(target.GetAccessor());
-
-    /**
-     * The "DJLSLosslessDecoder" and "DJLSNearLosslessDecoder" in DCMTK
-     * are exactly the same, except for the "supportedTransferSyntax()"
-     * virtual function.
-     * http://support.dcmtk.org/docs/classDJLSDecoderBase.html
-     **/
-
-    DJLSLosslessDecoder decoder; DJLSCodecParameter parameters;
-    //DJLSNearLosslessDecoder decoder; DJLSCodecParameter parameters;
-
-    Uint32 startFragment = 0;  // Default 
-    OFString decompressedColorModel;  // Out
-    DJ_RPLossless representationParameter;
-    OFCondition c = decoder.decodeFrame(&representationParameter, pixelSequence, &parameters, 
-                                        &dataset, frame, startFragment, targetAccessor.GetBuffer(), 
-                                        targetAccessor.GetSize(), decompressedColorModel);
-
-    if (!c.good())
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-#endif
-
-
-
-
-  bool DicomImageDecoder::Decode(ImageBuffer& target,
-                                 DcmDataset& dataset,
-                                 unsigned int frame)
-  {
-    if (IsUncompressedImage(dataset))
-    {
-      DecodeUncompressedImage(target, dataset, frame);
-      return true;
-    }
-
-
-#if ORTHANC_JPEG_LOSSLESS_ENABLED == 1
-    if (IsJpegLossless(dataset))
-    {
-      LOG(INFO) << "Decoding a JPEG-LS image";
-      DecodeJpegLossless(target, dataset, frame);
-      return true;
-    }
-#endif
-
-
-#if ORTHANC_JPEG_ENABLED == 1
-    // TODO Implement this part to speed up JPEG decompression
-#endif
-
-
-    /**
-     * This DICOM image format is not natively supported by
-     * Orthanc. As a last resort, try and decode it through
-     * DCMTK. This will result in higher memory consumption. This is
-     * actually the second example of the following page:
-     * http://support.dcmtk.org/docs/mod_dcmjpeg.html#Examples
-     **/
-    
-    {
-      LOG(INFO) << "Using DCMTK to decode a compressed image";
-
-      std::auto_ptr<DcmDataset> converted(dynamic_cast<DcmDataset*>(dataset.clone()));
-      converted->chooseRepresentation(EXS_LittleEndianExplicit, NULL);
-
-      if (converted->canWriteXfer(EXS_LittleEndianExplicit))
-      {
-        DecodeUncompressedImageInternal(target, *converted, frame);
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-
-  static bool IsColorImage(PixelFormat format)
-  {
-    return (format == PixelFormat_RGB24 ||
-            format == PixelFormat_RGBA32);
-  }
-
-
-  bool DicomImageDecoder::DecodeAndTruncate(ImageBuffer& target,
-                                            DcmDataset& dataset,
-                                            unsigned int frame,
-                                            PixelFormat format,
-                                            bool allowColorConversion)
-  {
-    // TODO Special case for uncompressed images
-    
-    ImageBuffer source;
-    if (!Decode(source, dataset, frame))
-    {
-      return false;
-    }
-
-    // If specified, prevent the conversion between color and
-    // grayscale images
-    bool isSourceColor = IsColorImage(source.GetFormat());
-    bool isTargetColor = IsColorImage(format);
-
-    if (!allowColorConversion)
-    {
-      if (isSourceColor ^ isTargetColor)
-      {
-        return false;
-      }
-    }
-
-    if (source.GetFormat() == format)
-    {
-      // No conversion is required, return the temporary image
-      target.AcquireOwnership(source);
-      return true;
-    }
-
-    target.SetFormat(format);
-    target.SetWidth(source.GetWidth());
-    target.SetHeight(source.GetHeight());
-
-    ImageAccessor targetAccessor(target.GetAccessor());
-    ImageAccessor sourceAccessor(source.GetAccessor());
-    ImageProcessing::Convert(targetAccessor, sourceAccessor);
-
-    return true;
-  }
-
-
-  bool DicomImageDecoder::DecodePreview(ImageBuffer& target,
-                                        DcmDataset& dataset,
-                                        unsigned int frame)
-  {
-    // TODO Special case for uncompressed images
-    
-    ImageBuffer source;
-    if (!Decode(source, dataset, frame))
-    {
-      return false;
-    }
-
-    switch (source.GetFormat())
-    {
-      case PixelFormat_RGB24:
-      {
-        // Directly return color images (RGB)
-        target.AcquireOwnership(source);
-        return true;
-      }
-
-      case PixelFormat_Grayscale8:
-      case PixelFormat_Grayscale16:
-      case PixelFormat_SignedGrayscale16:
-      {
-        // Grayscale image: Stretch its dynamics to the [0,255] range
-        target.SetFormat(PixelFormat_Grayscale8);
-        target.SetWidth(source.GetWidth());
-        target.SetHeight(source.GetHeight());
-
-        ImageAccessor targetAccessor(target.GetAccessor());
-        ImageAccessor sourceAccessor(source.GetAccessor());
-
-        int64_t a, b;
-        ImageProcessing::GetMinMaxValue(a, b, sourceAccessor);
-        
-        if (a == b)
-        {
-          ImageProcessing::Set(targetAccessor, 0);
-        }
-        else
-        {
-          ImageProcessing::ShiftScale(sourceAccessor, static_cast<float>(-a), 255.0f / static_cast<float>(b - a));
-
-          if (source.GetFormat() == PixelFormat_Grayscale8)
-          {
-            target.AcquireOwnership(source);
-          }
-          else
-          {
-            ImageProcessing::Convert(targetAccessor, sourceAccessor);
-          }
-        }
-
-        return true;
-      }
-      
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-}
--- a/OrthancServer/Internals/DicomImageDecoder.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include <dcmtk/dcmdata/dcfilefo.h>
-
-#include "../../Core/Images/ImageBuffer.h"
-
-namespace Orthanc
-{
-  class DicomImageDecoder
-  {
-  private:
-    class ImageSource;
-
-    static void DecodeUncompressedImageInternal(ImageBuffer& target,
-                                                DcmDataset& dataset,
-                                                unsigned int frame);
-
-    static bool IsPsmctRle1(DcmDataset& dataset);
-
-    static void SetupImageBuffer(ImageBuffer& target,
-                                 DcmDataset& dataset);
-
-    static bool IsUncompressedImage(const DcmDataset& dataset);
-
-    static void DecodeUncompressedImage(ImageBuffer& target,
-                                        DcmDataset& dataset,
-                                        unsigned int frame);
-
-#if ORTHANC_JPEG_LOSSLESS_ENABLED == 1
-    static void DecodeJpegLossless(ImageBuffer& target,
-                                   DcmDataset& dataset,
-                                   unsigned int frame);
-#endif
-
-  public:
-    static bool Decode(ImageBuffer& target,
-                       DcmDataset& dataset,
-                       unsigned int frame);
-
-    static bool DecodeAndTruncate(ImageBuffer& target,
-                                  DcmDataset& dataset,
-                                  unsigned int frame,
-                                  PixelFormat format,
-                                  bool allowColorConversion);
-
-    static bool DecodePreview(ImageBuffer& target,
-                              DcmDataset& dataset,
-                              unsigned int frame);
-  };
-}
--- a/OrthancServer/Internals/FindScp.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,203 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-
-/*=========================================================================
-
-  This file is based on portions of the following project:
-
-  Program: DCMTK 3.6.0
-  Module:  http://dicom.offis.de/dcmtk.php.en
-
-Copyright (C) 1994-2011, OFFIS e.V.
-All rights reserved.
-
-This software and supporting documentation were developed by
-
-  OFFIS e.V.
-  R&D Division Health
-  Escherweg 2
-  26121 Oldenburg, Germany
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
-- Redistributions of source code must retain the above copyright
-  notice, this list of conditions and the following disclaimer.
-
-- Redistributions in binary form must reproduce the above copyright
-  notice, this list of conditions and the following disclaimer in the
-  documentation and/or other materials provided with the distribution.
-
-- Neither the name of OFFIS nor the names of its contributors may be
-  used to endorse or promote products derived from this software
-  without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-=========================================================================*/
-
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "FindScp.h"
-
-#include "../FromDcmtkBridge.h"
-#include "../ToDcmtkBridge.h"
-#include "../../Core/Logging.h"
-#include "../../Core/OrthancException.h"
-
-
-
-namespace Orthanc
-{
-  namespace
-  {  
-    struct FindScpData
-    {
-      IFindRequestHandler* handler_;
-      DicomMap input_;
-      DicomFindAnswers answers_;
-      DcmDataset* lastRequest_;
-      const std::string* remoteIp_;
-      const std::string* remoteAet_;
-      bool noCroppingOfResults_;
-    };
-
-
-    void FindScpCallback(
-      /* in */ 
-      void *callbackData,  
-      OFBool cancelled, 
-      T_DIMSE_C_FindRQ *request, 
-      DcmDataset *requestIdentifiers, 
-      int responseCount,
-      /* out */
-      T_DIMSE_C_FindRSP *response,
-      DcmDataset **responseIdentifiers,
-      DcmDataset **statusDetail)
-    {
-      bzero(response, sizeof(T_DIMSE_C_FindRSP));
-      *statusDetail = NULL;
-
-      FindScpData& data = *reinterpret_cast<FindScpData*>(callbackData);
-      if (data.lastRequest_ == NULL)
-      {
-        FromDcmtkBridge::Convert(data.input_, *requestIdentifiers);
-
-        try
-        {
-          data.noCroppingOfResults_ = data.handler_->Handle(data.answers_, data.input_, 
-                                                            *data.remoteIp_, *data.remoteAet_);
-        }
-        catch (OrthancException& e)
-        {
-          // Internal error!
-          LOG(ERROR) <<  "C-FIND request handler has failed: " << e.What();
-          response->DimseStatus = STATUS_FIND_Failed_UnableToProcess;
-          *responseIdentifiers = NULL;   
-          return;
-        }
-
-        data.lastRequest_ = requestIdentifiers;
-      }
-      else if (data.lastRequest_ != requestIdentifiers)
-      {
-        // Internal error!
-        response->DimseStatus = STATUS_FIND_Failed_UnableToProcess;
-        *responseIdentifiers = NULL;   
-        return;
-      }
-
-      if (responseCount <= static_cast<int>(data.answers_.GetSize()))
-      {
-        // There are pending results that are still to be sent
-        response->DimseStatus = STATUS_Pending;
-        *responseIdentifiers = ToDcmtkBridge::Convert(data.answers_.GetAnswer(responseCount - 1));
-      }
-      else if (data.noCroppingOfResults_)
-      {
-        // Success: All the results have been sent
-        response->DimseStatus = STATUS_Success;
-        *responseIdentifiers = NULL;
-      }
-      else
-      {
-        // Success, but the results were too numerous and had to be cropped
-        LOG(WARNING) <<  "Too many results for an incoming C-FIND query";
-        response->DimseStatus = STATUS_FIND_Cancel_MatchingTerminatedDueToCancelRequest;
-        *responseIdentifiers = NULL;
-      }
-    }
-  }
-
-
-  OFCondition Internals::findScp(T_ASC_Association * assoc, 
-                                 T_DIMSE_Message * msg, 
-                                 T_ASC_PresentationContextID presID,
-                                 IFindRequestHandler& handler,
-                                 const std::string& remoteIp,
-                                 const std::string& remoteAet)
-  {
-    FindScpData data;
-    data.lastRequest_ = NULL;
-    data.handler_ = &handler;
-    data.remoteIp_ = &remoteIp;
-    data.remoteAet_ = &remoteAet;
-    data.noCroppingOfResults_ = true;
-
-    OFCondition cond = DIMSE_findProvider(assoc, presID, &msg->msg.CFindRQ, 
-                                          FindScpCallback, &data,
-                                          /*opt_blockMode*/ DIMSE_BLOCKING, 
-                                          /*opt_dimse_timeout*/ 0);
-
-    // if some error occured, dump corresponding information and remove the outfile if necessary
-    if (cond.bad())
-    {
-      OFString temp_str;
-      LOG(ERROR) << "Find SCP Failed: " << cond.text();
-    }
-
-    return cond;
-  }
-}
--- a/OrthancServer/Internals/FindScp.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../DicomProtocol/IFindRequestHandler.h"
-
-#include <dcmtk/dcmnet/dimse.h>
-
-namespace Orthanc
-{
-  namespace Internals
-  {
-    OFCondition findScp(T_ASC_Association * assoc, 
-                        T_DIMSE_Message * msg, 
-                        T_ASC_PresentationContextID presID,
-                        IFindRequestHandler& handler,
-                        const std::string& remoteIp,
-                        const std::string& remoteAet);
-  }
-}
--- a/OrthancServer/Internals/MoveScp.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,241 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-
-
-/*=========================================================================
-
-  This file is based on portions of the following project:
-
-  Program: DCMTK 3.6.0
-  Module:  http://dicom.offis.de/dcmtk.php.en
-
-Copyright (C) 1994-2011, OFFIS e.V.
-All rights reserved.
-
-This software and supporting documentation were developed by
-
-  OFFIS e.V.
-  R&D Division Health
-  Escherweg 2
-  26121 Oldenburg, Germany
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
-- Redistributions of source code must retain the above copyright
-  notice, this list of conditions and the following disclaimer.
-
-- Redistributions in binary form must reproduce the above copyright
-  notice, this list of conditions and the following disclaimer in the
-  documentation and/or other materials provided with the distribution.
-
-- Neither the name of OFFIS nor the names of its contributors may be
-  used to endorse or promote products derived from this software
-  without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-=========================================================================*/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "MoveScp.h"
-
-#include <memory>
-
-#include "../FromDcmtkBridge.h"
-#include "../ToDcmtkBridge.h"
-#include "../../Core/Logging.h"
-#include "../../Core/OrthancException.h"
-
-
-namespace Orthanc
-{
-  namespace
-  {  
-    struct MoveScpData
-    {
-      std::string target_;
-      IMoveRequestHandler* handler_;
-      DicomMap input_;
-      DcmDataset* lastRequest_;
-      unsigned int subOperationCount_;
-      unsigned int failureCount_;
-      unsigned int warningCount_;
-      std::auto_ptr<IMoveRequestIterator> iterator_;
-      const std::string* remoteIp_;
-      const std::string* remoteAet_;
-    };
-
-
-    void MoveScpCallback(
-      /* in */ 
-      void *callbackData,  
-      OFBool cancelled, 
-      T_DIMSE_C_MoveRQ *request, 
-      DcmDataset *requestIdentifiers, 
-      int responseCount,
-      /* out */
-      T_DIMSE_C_MoveRSP *response,
-      DcmDataset **responseIdentifiers,
-      DcmDataset **statusDetail)
-    {
-      bzero(response, sizeof(T_DIMSE_C_MoveRSP));
-      *statusDetail = NULL;
-      *responseIdentifiers = NULL;   
-
-      MoveScpData& data = *reinterpret_cast<MoveScpData*>(callbackData);
-      if (data.lastRequest_ == NULL)
-      {
-        FromDcmtkBridge::Convert(data.input_, *requestIdentifiers);
-
-        try
-        {
-          data.iterator_.reset(data.handler_->Handle(data.target_, data.input_, 
-                                                     *data.remoteIp_, *data.remoteAet_));
-
-          if (data.iterator_.get() == NULL)
-          {
-            // Internal error!
-            response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess;
-            return;
-          }
-
-          data.subOperationCount_ = data.iterator_->GetSubOperationCount();
-          data.failureCount_ = 0;
-          data.warningCount_ = 0;
-        }
-        catch (OrthancException& e)
-        {
-          // Internal error!
-          LOG(ERROR) << "IMoveRequestHandler Failed: " << e.What();
-          response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess;
-          return;
-        }
-
-        data.lastRequest_ = requestIdentifiers;
-      }
-      else if (data.lastRequest_ != requestIdentifiers)
-      {
-        // Internal error!
-        response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess;
-        return;
-      }
-  
-      if (data.subOperationCount_ == 0)
-      {
-        response->DimseStatus = STATUS_Success;
-      }
-      else
-      {
-        IMoveRequestIterator::Status status;
-
-        try
-        {
-          status = data.iterator_->DoNext();
-        }
-        catch (OrthancException& e)
-        {
-          // Internal error!
-          LOG(ERROR) << "IMoveRequestHandler Failed: " << e.What();
-          response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess;
-          return;
-        }
-
-        if (status == IMoveRequestIterator::Status_Failure)
-        {
-          data.failureCount_++;
-        }
-        else if (status == IMoveRequestIterator::Status_Warning)
-        {
-          data.warningCount_++;
-        }
-
-        if (responseCount < static_cast<int>(data.subOperationCount_))
-        {
-          response->DimseStatus = STATUS_Pending;
-        }
-        else
-        {
-          response->DimseStatus = STATUS_Success;
-        }
-      }
-
-      response->NumberOfRemainingSubOperations = data.subOperationCount_ - responseCount;
-      response->NumberOfCompletedSubOperations = responseCount;
-      response->NumberOfFailedSubOperations = data.failureCount_;
-      response->NumberOfWarningSubOperations = data.warningCount_;
-    }
-  }
-
-
-  OFCondition Internals::moveScp(T_ASC_Association * assoc, 
-                                 T_DIMSE_Message * msg, 
-                                 T_ASC_PresentationContextID presID,
-                                 IMoveRequestHandler& handler,
-                                 const std::string& remoteIp,
-                                 const std::string& remoteAet)
-  {
-    MoveScpData data;
-    data.target_ = std::string(msg->msg.CMoveRQ.MoveDestination);
-    data.lastRequest_ = NULL;
-    data.handler_ = &handler;
-    data.remoteIp_ = &remoteIp;
-    data.remoteAet_ = &remoteAet;
-
-    OFCondition cond = DIMSE_moveProvider(assoc, presID, &msg->msg.CMoveRQ, 
-                                          MoveScpCallback, &data,
-                                          /*opt_blockMode*/ DIMSE_BLOCKING, 
-                                          /*opt_dimse_timeout*/ 0);
-
-    // if some error occured, dump corresponding information and remove the outfile if necessary
-    if (cond.bad())
-    {
-      OFString temp_str;
-      LOG(ERROR) << "Move SCP Failed: " << cond.text();
-    }
-
-    return cond;
-  }
-}
--- a/OrthancServer/Internals/MoveScp.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../DicomProtocol/IMoveRequestHandler.h"
-
-#include <dcmtk/dcmnet/dimse.h>
-
-namespace Orthanc
-{
-  namespace Internals
-  {
-    OFCondition moveScp(T_ASC_Association * assoc, 
-                        T_DIMSE_Message * msg, 
-                        T_ASC_PresentationContextID presID,
-                        IMoveRequestHandler& handler,
-                        const std::string& remoteIp,
-                        const std::string& remoteAet);
-  }
-}
--- a/OrthancServer/Internals/StoreScp.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,301 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-
-
-/*=========================================================================
-
-  This file is based on portions of the following project:
-
-  Program: DCMTK 3.6.0
-  Module:  http://dicom.offis.de/dcmtk.php.en
-
-Copyright (C) 1994-2011, OFFIS e.V.
-All rights reserved.
-
-This software and supporting documentation were developed by
-
-  OFFIS e.V.
-  R&D Division Health
-  Escherweg 2
-  26121 Oldenburg, Germany
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
-- Redistributions of source code must retain the above copyright
-  notice, this list of conditions and the following disclaimer.
-
-- Redistributions in binary form must reproduce the above copyright
-  notice, this list of conditions and the following disclaimer in the
-  documentation and/or other materials provided with the distribution.
-
-- Neither the name of OFFIS nor the names of its contributors may be
-  used to endorse or promote products derived from this software
-  without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-=========================================================================*/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "StoreScp.h"
-
-#include "../FromDcmtkBridge.h"
-#include "../ServerToolbox.h"
-#include "../ToDcmtkBridge.h"
-#include "../../Core/OrthancException.h"
-#include "../../Core/Logging.h"
-
-#include <dcmtk/dcmdata/dcfilefo.h>
-#include <dcmtk/dcmdata/dcmetinf.h>
-#include <dcmtk/dcmdata/dcostrmb.h>
-#include <dcmtk/dcmdata/dcdeftag.h>
-#include <dcmtk/dcmnet/diutil.h>
-
-
-namespace Orthanc
-{
-  namespace
-  {  
-    struct StoreCallbackData
-    {
-      IStoreRequestHandler* handler;
-      const std::string* remoteIp;
-      const char* remoteAET;
-      const char* calledAET;
-      const char* modality;
-      const char* affectedSOPInstanceUID;
-      uint32_t messageID;
-    };
-
-    
-    static void
-    storeScpCallback(
-      void *callbackData,
-      T_DIMSE_StoreProgress *progress,
-      T_DIMSE_C_StoreRQ *req,
-      char * /*imageFileName*/, DcmDataset **imageDataSet,
-      T_DIMSE_C_StoreRSP *rsp,
-      DcmDataset **statusDetail)
-    /*
-     * This function.is used to indicate progress when storescp receives instance data over the
-     * network. On the final call to this function (identified by progress->state == DIMSE_StoreEnd)
-     * this function will store the data set which was received over the network to a file.
-     * Earlier calls to this function will simply cause some information to be dumped to stdout.
-     *
-     * Parameters:
-     *   callbackData  - [in] data for this callback function
-     *   progress      - [in] The state of progress. (identifies if this is the initial or final call
-     *                   to this function, or a call in between these two calls.
-     *   req           - [in] The original store request message.
-     *   imageFileName - [in] The path to and name of the file the information shall be written to.
-     *   imageDataSet  - [in] The data set which shall be stored in the image file
-     *   rsp           - [inout] the C-STORE-RSP message (will be sent after the call to this function)
-     *   statusDetail  - [inout] This variable can be used to capture detailed information with regard to
-     *                   the status information which is captured in the status element (0000,0900). Note
-     *                   that this function does specify any such information, the pointer will be set to NULL.
-     */
-    {
-      StoreCallbackData *cbdata = OFstatic_cast(StoreCallbackData *, callbackData);
-
-      DIC_UI sopClass;
-      DIC_UI sopInstance;
-
-      // if this is the final call of this function, save the data which was received to a file
-      // (note that we could also save the image somewhere else, put it in database, etc.)
-      if (progress->state == DIMSE_StoreEnd)
-      {
-        OFString tmpStr;
-
-        // do not send status detail information
-        *statusDetail = NULL;
-
-        // Concerning the following line: an appropriate status code is already set in the resp structure,
-        // it need not be success. For example, if the caller has already detected an out of resources problem
-        // then the status will reflect this.  The callback function is still called to allow cleanup.
-        //rsp->DimseStatus = STATUS_Success;
-
-        // we want to write the received information to a file only if this information
-        // is present and the options opt_bitPreserving and opt_ignore are not set.
-        if ((imageDataSet != NULL) && (*imageDataSet != NULL))
-        {
-          DicomMap summary;
-          Json::Value dicomJson;
-          std::string buffer;
-
-          try
-          {
-            FromDcmtkBridge::Convert(summary, **imageDataSet);
-            FromDcmtkBridge::ToJson(dicomJson, **imageDataSet,
-                                    DicomToJsonFormat_Full, 
-                                    DicomToJsonFlags_Default, 
-                                    256 /* max string length */);
-
-            if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, **imageDataSet))
-            {
-              LOG(ERROR) << "cannot write DICOM file to memory";
-              rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources;
-            }
-          }
-          catch (...)
-          {
-            rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources;
-          }
-
-          // check the image to make sure it is consistent, i.e. that its sopClass and sopInstance correspond
-          // to those mentioned in the request. If not, set the status in the response message variable.
-          if (rsp->DimseStatus == STATUS_Success)
-          {
-            // which SOP class and SOP instance ?
-            if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sopInstance, /*opt_correctUIDPadding*/ OFFalse))
-            {
-              //LOG4CPP_ERROR(Internals::GetLogger(), "bad DICOM file: " << fileName);
-              rsp->DimseStatus = STATUS_STORE_Error_CannotUnderstand;
-            }
-            else if (strcmp(sopClass, req->AffectedSOPClassUID) != 0)
-            {
-              rsp->DimseStatus = STATUS_STORE_Error_DataSetDoesNotMatchSOPClass;
-            }
-            else if (strcmp(sopInstance, req->AffectedSOPInstanceUID) != 0)
-            {
-              rsp->DimseStatus = STATUS_STORE_Error_DataSetDoesNotMatchSOPClass;
-            }
-            else
-            {
-              try
-              {
-                cbdata->handler->Handle(buffer, summary, dicomJson, *cbdata->remoteIp, cbdata->remoteAET, cbdata->calledAET);
-              }
-              catch (OrthancException& e)
-              {
-                rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources;
-
-                if (e.GetErrorCode() == ErrorCode_InexistentTag)
-                {
-                  Toolbox::LogMissingRequiredTag(summary);
-                }
-                else
-                {
-                  LOG(ERROR) << "Exception while storing DICOM: " << e.What();
-                }
-              }
-            }
-          }
-        }
-      }
-    }
-  }
-
-/*
- * This function processes a DIMSE C-STORE-RQ commmand that was
- * received over the network connection.
- *
- * Parameters:
- *   assoc  - [in] The association (network connection to another DICOM application).
- *   msg    - [in] The DIMSE C-STORE-RQ message that was received.
- *   presID - [in] The ID of the presentation context which was specified in the PDV which contained
- *                 the DIMSE command.
- */
-  OFCondition Internals::storeScp(T_ASC_Association * assoc, 
-                                  T_DIMSE_Message * msg, 
-                                  T_ASC_PresentationContextID presID,
-                                  IStoreRequestHandler& handler,
-                                  const std::string& remoteIp)
-  {
-    OFCondition cond = EC_Normal;
-    T_DIMSE_C_StoreRQ *req;
-
-    // assign the actual information of the C-STORE-RQ command to a local variable
-    req = &msg->msg.CStoreRQ;
-
-    // intialize some variables
-    StoreCallbackData data;
-    data.handler = &handler;
-    data.remoteIp = &remoteIp;
-    data.modality = dcmSOPClassUIDToModality(req->AffectedSOPClassUID/*, "UNKNOWN"*/);
-    if (data.modality == NULL)
-      data.modality = "UNKNOWN";
-
-    data.affectedSOPInstanceUID = req->AffectedSOPInstanceUID;
-    data.messageID = req->MessageID;
-    if (assoc && assoc->params)
-    {
-      data.remoteAET = assoc->params->DULparams.callingAPTitle;
-      data.calledAET = assoc->params->DULparams.calledAPTitle;
-    }
-    else
-    {
-      data.remoteAET = "";
-      data.calledAET = "";
-    }
-
-    DcmFileFormat dcmff;
-
-    // store SourceApplicationEntityTitle in metaheader
-    if (assoc && assoc->params)
-    {
-      const char *aet = assoc->params->DULparams.callingAPTitle;
-      if (aet) dcmff.getMetaInfo()->putAndInsertString(DCM_SourceApplicationEntityTitle, aet);
-    }
-
-    // define an address where the information which will be received over the network will be stored
-    DcmDataset *dset = dcmff.getDataset();
-
-    cond = DIMSE_storeProvider(assoc, presID, req, NULL, /*opt_useMetaheader*/OFFalse, &dset,
-                               storeScpCallback, &data, 
-                               /*opt_blockMode*/ DIMSE_BLOCKING, 
-                               /*opt_dimse_timeout*/ 0);
-
-    // if some error occured, dump corresponding information and remove the outfile if necessary
-    if (cond.bad())
-    {
-      OFString temp_str;
-      LOG(ERROR) << "Store SCP Failed: " << cond.text();
-    }
-
-    // return return value
-    return cond;
-  }
-}
--- a/OrthancServer/Internals/StoreScp.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../DicomProtocol/IStoreRequestHandler.h"
-
-#include <dcmtk/dcmnet/dimse.h>
-
-namespace Orthanc
-{
-  namespace Internals
-  {
-    OFCondition storeScp(T_ASC_Association * assoc, 
-                         T_DIMSE_Message * msg, 
-                         T_ASC_PresentationContextID presID,
-                         IStoreRequestHandler& handler,
-                         const std::string& remoteIp);
-  }
-}
--- a/OrthancServer/LuaScripting.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/LuaScripting.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,24 +34,217 @@
 #include "PrecompiledHeadersServer.h"
 #include "LuaScripting.h"
 
+#include "OrthancInitialization.h"
+#include "OrthancRestApi/OrthancRestApi.h"
 #include "ServerContext.h"
-#include "OrthancInitialization.h"
-#include "../Core/Lua/LuaFunctionCall.h"
+
 #include "../Core/HttpServer/StringHttpOutput.h"
 #include "../Core/Logging.h"
-
-#include "Scheduler/DeleteInstanceCommand.h"
-#include "Scheduler/StoreScuCommand.h"
-#include "Scheduler/StorePeerCommand.h"
-#include "Scheduler/ModifyInstanceCommand.h"
-#include "Scheduler/CallSystemCommand.h"
-#include "OrthancRestApi/OrthancRestApi.h"
+#include "../Core/Lua/LuaFunctionCall.h"
 
 #include <EmbeddedResources.h>
 
 
 namespace Orthanc
 {
+  class LuaScripting::IEvent : public IDynamicObject
+  {
+  public:
+    virtual void Apply(LuaScripting& lock) = 0;
+  };
+
+
+  class LuaScripting::OnStoredInstanceEvent : public LuaScripting::IEvent
+  {
+  private:
+    std::string    instanceId_;
+    Json::Value    simplifiedTags_;
+    Json::Value    metadata_;
+    Json::Value    origin_;
+
+  public:
+    OnStoredInstanceEvent(const std::string& instanceId,
+                          const Json::Value& simplifiedTags,
+                          const Json::Value& metadata,
+                          const DicomInstanceToStore& instance) :
+      instanceId_(instanceId),
+      simplifiedTags_(simplifiedTags),
+      metadata_(metadata)
+    {
+      instance.GetOrigin().Format(origin_);
+    }
+
+    virtual void Apply(LuaScripting& that)
+    {
+      static const char* NAME = "OnStoredInstance";
+
+      LuaScripting::Lock lock(that);
+
+      if (lock.GetLua().IsExistingFunction(NAME))
+      {
+        that.InitializeJob();
+
+        LuaFunctionCall call(lock.GetLua(), NAME);
+        call.PushString(instanceId_);
+        call.PushJson(simplifiedTags_);
+        call.PushJson(metadata_);
+        call.PushJson(origin_);
+        call.Execute();
+
+        that.SubmitJob();
+      }
+    }
+  };
+
+
+  class LuaScripting::ExecuteEvent : public LuaScripting::IEvent
+  {
+  private:
+    std::string    command_;
+
+  public:
+    ExecuteEvent(const std::string& command) :
+      command_(command)
+    {
+    }
+
+    virtual void Apply(LuaScripting& that)
+    {
+      LuaScripting::Lock lock(that);
+
+      if (lock.GetLua().IsExistingFunction(command_.c_str()))
+      {
+        LuaFunctionCall call(lock.GetLua(), command_.c_str());
+        call.Execute();
+      }
+    }
+  };
+
+
+  class LuaScripting::StableResourceEvent : public LuaScripting::IEvent
+  {
+  private:
+    ServerIndexChange  change_;
+
+  public:
+    StableResourceEvent(const ServerIndexChange& change) :
+    change_(change)
+    {
+    }
+
+    virtual void Apply(LuaScripting& that)
+    {
+      const char* name;
+
+      switch (change_.GetChangeType())
+      {
+        case ChangeType_StablePatient:
+          name = "OnStablePatient";
+          break;
+
+        case ChangeType_StableStudy:
+          name = "OnStableStudy";
+          break;
+
+        case ChangeType_StableSeries:
+          name = "OnStableSeries";
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      {
+        // Avoid unnecessary calls to the database if there's no Lua callback
+        LuaScripting::Lock lock(that);
+
+        if (!lock.GetLua().IsExistingFunction(name))
+        {
+          return;
+        }
+      }
+      
+      Json::Value tags, metadata;
+      if (that.context_.GetIndex().LookupResource(tags, change_.GetPublicId(), change_.GetResourceType()) &&
+          that.context_.GetIndex().GetMetadata(metadata, change_.GetPublicId()))
+      {
+        LuaScripting::Lock lock(that);
+
+        if (lock.GetLua().IsExistingFunction(name))
+        {
+          that.InitializeJob();
+
+          LuaFunctionCall call(lock.GetLua(), name);
+          call.PushString(change_.GetPublicId());
+          call.PushJson(tags["MainDicomTags"]);
+          call.PushJson(metadata);
+          call.Execute();
+
+          that.SubmitJob();
+        }
+      }
+    }
+  };
+
+
+  class LuaScripting::JobEvent : public LuaScripting::IEvent
+  {
+  public:
+    enum Type
+    {
+      Type_Failure,
+      Type_Submitted,
+      Type_Success
+    };
+    
+  private:
+    Type         type_;
+    std::string  jobId_;
+
+  public:
+    JobEvent(Type type,
+             const std::string& jobId) :
+      type_(type),
+      jobId_(jobId)
+    {
+    }
+
+    virtual void Apply(LuaScripting& that)
+    {
+      std::string functionName;
+      
+      switch (type_)
+      {
+        case Type_Failure:
+          functionName = "OnJobFailure";
+          break;
+
+        case Type_Submitted:
+          functionName = "OnJobSubmitted";
+          break;
+
+        case Type_Success:
+          functionName = "OnJobSuccess";
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      {
+        LuaScripting::Lock lock(that);
+
+        if (lock.GetLua().IsExistingFunction(functionName.c_str()))
+        {
+          LuaFunctionCall call(lock.GetLua(), functionName.c_str());
+          call.PushString(jobId_);
+          call.Execute();
+        }
+      }
+    }
+  };
+
+
   ServerContext* LuaScripting::GetServerContext(lua_State *state)
   {
     const void* value = LuaContext::GetGlobalVariable(state, "_ServerContext");
@@ -210,7 +404,7 @@
     }
 
     LOG(ERROR) << "Lua: Error in RestApiDelete() for URI: " << uri;
-      lua_pushnil(state);
+    lua_pushnil(state);
 
     return 1;
   }
@@ -228,13 +422,14 @@
   }
 
 
-  IServerCommand* LuaScripting::ParseOperation(const std::string& operation,
-                                               const Json::Value& parameters)
+  size_t LuaScripting::ParseOperation(LuaJobManager::Lock& lock,
+                                      const std::string& operation,
+                                      const Json::Value& parameters)
   {
     if (operation == "delete")
     {
       LOG(INFO) << "Lua script to delete resource " << parameters["Resource"].asString();
-      return new DeleteInstanceCommand(context_);
+      return lock.AddDeleteResourceOperation(context_);
     }
 
     if (operation == "store-scu")
@@ -249,34 +444,35 @@
         localAet = context_.GetDefaultLocalApplicationEntityTitle();
       }
 
-      std::string modality = parameters["Modality"].asString();
-      LOG(INFO) << "Lua script to send resource " << parameters["Resource"].asString()
-                << " to modality " << modality << " using Store-SCU";
-      return new StoreScuCommand(context_, localAet,
-                                 Configuration::GetModalityUsingSymbolicName(modality), true);
+      std::string name = parameters["Modality"].asString();
+      RemoteModalityParameters modality = Configuration::GetModalityUsingSymbolicName(name);
+
+      // This is not a C-MOVE: No need to call "StoreScuCommand::SetMoveOriginator()"
+      return lock.AddStoreScuOperation(localAet, modality);
     }
 
     if (operation == "store-peer")
     {
-      std::string peer = parameters["Peer"].asString();
-      LOG(INFO) << "Lua script to send resource " << parameters["Resource"].asString()
-                << " to peer " << peer << " using HTTP";
+      std::string name = parameters["Peer"].asString();
 
-      OrthancPeerParameters parameters;
-      Configuration::GetOrthancPeer(parameters, peer);
-      return new StorePeerCommand(context_, parameters, true);
+      WebServiceParameters peer;
+      if (Configuration::GetOrthancPeer(peer, name))
+      {
+        return lock.AddStorePeerOperation(peer);
+      }
+      else
+      {
+        LOG(ERROR) << "No peer with symbolic name: " << name;
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
     }
 
     if (operation == "modify")
     {
-      LOG(INFO) << "Lua script to modify resource " << parameters["Resource"].asString();
       std::auto_ptr<DicomModification> modification(new DicomModification);
-      OrthancRestApi::ParseModifyRequest(*modification, parameters);
+      modification->ParseModifyRequest(parameters);
 
-      std::auto_ptr<ModifyInstanceCommand> command
-        (new ModifyInstanceCommand(context_, RequestOrigin_Lua, modification.release()));
-
-      return command.release();
+      return lock.AddModifyInstanceOperation(context_, modification.release());
     }
 
     if (operation == "call-system")
@@ -317,7 +513,10 @@
         }
       }
 
-      return new CallSystemCommand(context_, parameters["Command"].asString(), args);
+      std::string command = parameters["Command"].asString();
+      std::vector<std::string> postArgs;
+
+      return lock.AddSystemCallOperation(command, args, postArgs);
     }
 
     throw OrthancException(ErrorCode_ParameterOutOfRange);
@@ -330,7 +529,7 @@
   }
 
 
-  void LuaScripting::SubmitJob(const std::string& description)
+  void LuaScripting::SubmitJob()
   {
     Json::Value operations;
     LuaFunctionCall call2(lua_, "_AccessJob");
@@ -341,8 +540,10 @@
       throw OrthancException(ErrorCode_InternalError);
     }
 
-    ServerJob job;
-    ServerCommandInstance* previousCommand = NULL;
+    LuaJobManager::Lock lock(jobManager_, context_.GetJobsEngine());
+
+    bool isFirst = true;
+    size_t previous = 0;  // Dummy initialization to avoid warning
 
     for (Json::Value::ArrayIndex i = 0; i < operations.size(); ++i)
     {
@@ -353,34 +554,33 @@
       }
 
       const Json::Value& parameters = operations[i];
-      std::string operation = parameters["Operation"].asString();
-
-      ServerCommandInstance& command = job.AddCommand(ParseOperation(operation, operations[i]));
-        
       if (!parameters.isMember("Resource"))
       {
         throw OrthancException(ErrorCode_InternalError);
       }
 
+      std::string operation = parameters["Operation"].asString();
+      size_t index = ParseOperation(lock, operation, operations[i]);
+        
       std::string resource = parameters["Resource"].asString();
-      if (resource.empty())
+      if (!resource.empty())
       {
-        previousCommand->ConnectOutput(command);
+        lock.AddDicomInstanceInput(index, context_, resource);
       }
-      else 
+      else if (!isFirst)
       {
-        command.AddInput(resource);
+        lock.Connect(previous, index);
       }
 
-      previousCommand = &command;
+      isFirst = false;
+      previous = index;
     }
-
-    job.SetDescription(description);
-    context_.GetScheduler().Submit(job);
   }
 
 
-  LuaScripting::LuaScripting(ServerContext& context) : context_(context)
+  LuaScripting::LuaScripting(ServerContext& context) : 
+    context_(context),
+    state_(State_Setup)
   {
     lua_.SetGlobalVariable("_ServerContext", &context);
     lua_.RegisterFunction("RestApiGet", RestApiGet);
@@ -389,34 +589,90 @@
     lua_.RegisterFunction("RestApiDelete", RestApiDelete);
     lua_.RegisterFunction("GetOrthancConfiguration", GetOrthancConfiguration);
 
-    lua_.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX);
-    lua_.SetHttpProxy(Configuration::GetGlobalStringParameter("HttpProxy", ""));
+    LOG(INFO) << "Initializing Lua for the event handler";
+    LoadGlobalConfiguration();
+  }
+
+
+  LuaScripting::~LuaScripting()
+  {
+    if (state_ == State_Running)
+    {
+      LOG(ERROR) << "INTERNAL ERROR: LuaScripting::Stop() should be invoked manually to avoid mess in the destruction order!";
+      Stop();
+    }
   }
 
 
-  void LuaScripting::ApplyOnStoredInstance(const std::string& instanceId,
-                                           const Json::Value& simplifiedTags,
-                                           const Json::Value& metadata,
-                                           const DicomInstanceToStore& instance)
+  void LuaScripting::EventThread(LuaScripting* that)
   {
-    static const char* NAME = "OnStoredInstance";
+    for (;;)
+    {
+      std::auto_ptr<IDynamicObject> event(that->pendingEvents_.Dequeue(100));
+
+      if (event.get() == NULL)
+      {
+        // The event queue is empty, check whether we should stop
+        boost::recursive_mutex::scoped_lock lock(that->mutex_);
 
-    if (lua_.IsExistingFunction(NAME))
-    {
-      InitializeJob();
+        if (that->state_ != State_Running)
+        {
+          return;
+        }
+      }
+      else
+      {
+        try
+        {
+          dynamic_cast<IEvent&>(*event).Apply(*that);
+        }
+        catch (OrthancException& e)
+        {
+          LOG(ERROR) << "Error while processing Lua events: " << e.What();
+        }
+      }
+    }
+  }
+
+
+  void LuaScripting::Start()
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
 
-      LuaFunctionCall call(lua_, NAME);
-      call.PushString(instanceId);
-      call.PushJson(simplifiedTags);
-      call.PushJson(metadata);
+    if (state_ != State_Setup ||
+        eventThread_.joinable()  /* already started */)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      LOG(INFO) << "Starting the Lua engine";
+      eventThread_ = boost::thread(EventThread, this);
+      state_ = State_Running;
+    }
+  }
+
 
-      Json::Value origin;
-      instance.GetOriginInformation(origin);
-      call.PushJson(origin);
+  void LuaScripting::Stop()
+  {
+    {
+      boost::recursive_mutex::scoped_lock lock(mutex_);
+
+      if (state_ != State_Running)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
 
-      call.Execute();
+      state_ = State_Done;
+    }
+
+    jobManager_.AwakeTrailingSleep();
 
-      SubmitJob(std::string("Lua script: ") + NAME);
+    if (eventThread_.joinable())
+    {
+      LOG(INFO) << "Stopping the Lua engine";
+      eventThread_.join();
+      LOG(INFO) << "The Lua engine has stopped";
     }
   }
 
@@ -425,8 +681,6 @@
                                           DicomInstanceToStore& instance,
                                           const Json::Value& simplifiedTags)
   {
-    boost::recursive_mutex::scoped_lock lock(mutex_);
-
     Json::Value metadata = Json::objectValue;
 
     for (ServerIndex::MetadataMap::const_iterator 
@@ -439,64 +693,17 @@
       }
     }
 
-    ApplyOnStoredInstance(publicId, simplifiedTags, metadata, instance);
+    pendingEvents_.Enqueue(new OnStoredInstanceEvent(publicId, simplifiedTags, metadata, instance));
   }
 
 
-  
-  void LuaScripting::OnStableResource(const ServerIndexChange& change)
-  {
-    const char* name;
-
-    switch (change.GetChangeType())
-    {
-      case ChangeType_StablePatient:
-        name = "OnStablePatient";
-        break;
-
-      case ChangeType_StableStudy:
-        name = "OnStableStudy";
-        break;
-
-      case ChangeType_StableSeries:
-        name = "OnStableSeries";
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-
-    Json::Value tags, metadata;
-    if (context_.GetIndex().LookupResource(tags, change.GetPublicId(), change.GetResourceType()) &&
-        context_.GetIndex().GetMetadata(metadata, change.GetPublicId()))
-    {
-      boost::recursive_mutex::scoped_lock lock(mutex_);
-
-      if (lua_.IsExistingFunction(name))
-      {
-        InitializeJob();
-
-        LuaFunctionCall call(lua_, name);
-        call.PushString(change.GetPublicId());
-        call.PushJson(tags["MainDicomTags"]);
-        call.PushJson(metadata);
-        call.Execute();
-
-        SubmitJob(std::string("Lua script: ") + name);
-      }
-    }
-  }
-
-
-
   void LuaScripting::SignalChange(const ServerIndexChange& change)
   {
     if (change.GetChangeType() == ChangeType_StablePatient ||
         change.GetChangeType() == ChangeType_StableStudy ||
         change.GetChangeType() == ChangeType_StableSeries)
     {
-      OnStableResource(change);
+      pendingEvents_.Enqueue(new StableResourceEvent(change));
     }
   }
 
@@ -514,7 +721,7 @@
       call.PushJson(simplified);
 
       Json::Value origin;
-      instance.GetOriginInformation(origin);
+      instance.GetOrigin().Format(origin);
       call.PushJson(origin);
 
       if (!call.ExecutePredicate())
@@ -529,12 +736,46 @@
 
   void LuaScripting::Execute(const std::string& command)
   {
-    LuaScripting::Locker locker(*this);
-      
-    if (locker.GetLua().IsExistingFunction(command.c_str()))
+    pendingEvents_.Enqueue(new ExecuteEvent(command));
+  }
+
+
+  void LuaScripting::LoadGlobalConfiguration()
+  {
+    lua_.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX);
+
+    std::list<std::string> luaScripts;
+    Configuration::GetGlobalListOfStringsParameter(luaScripts, "LuaScripts");
+
+    LuaScripting::Lock lock(*this);
+
+    for (std::list<std::string>::const_iterator
+           it = luaScripts.begin(); it != luaScripts.end(); ++it)
     {
-      LuaFunctionCall call(locker.GetLua(), command.c_str());
-      call.Execute();
+      std::string path = Configuration::InterpretStringParameterAsPath(*it);
+      LOG(INFO) << "Installing the Lua scripts from: " << path;
+      std::string script;
+      SystemToolbox::ReadFile(script, path);
+
+      lock.GetLua().Execute(script);
     }
   }
+
+  
+  void LuaScripting::SignalJobSubmitted(const std::string& jobId)
+  {
+    pendingEvents_.Enqueue(new JobEvent(JobEvent::Type_Submitted, jobId));
+  }
+  
+
+  void LuaScripting::SignalJobSuccess(const std::string& jobId)
+  {
+    pendingEvents_.Enqueue(new JobEvent(JobEvent::Type_Success, jobId));
+  }
+  
+
+  void LuaScripting::SignalJobFailure(const std::string& jobId)
+  {
+    pendingEvents_.Enqueue(new JobEvent(JobEvent::Type_Failure, jobId));
+  }
 }
--- a/OrthancServer/LuaScripting.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/LuaScripting.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,17 +33,32 @@
 
 #pragma once
 
-#include "IServerListener.h"
+#include "DicomInstanceToStore.h"
+#include "ServerJobs/LuaJobManager.h"
+
+#include "../Core/MultiThreading/SharedMessageQueue.h"
 #include "../Core/Lua/LuaContext.h"
-#include "Scheduler/IServerCommand.h"
 
 namespace Orthanc
 {
   class ServerContext;
 
-  class LuaScripting : public IServerListener
+  class LuaScripting : public boost::noncopyable
   {
   private:
+    enum State
+    {
+      State_Setup,
+      State_Running,
+      State_Done
+    };
+    
+    class ExecuteEvent;
+    class IEvent;
+    class OnStoredInstanceEvent;
+    class StableResourceEvent;
+    class JobEvent;
+
     static ServerContext* GetServerContext(lua_State *state);
 
     static int RestApiPostOrPut(lua_State *state,
@@ -53,33 +69,35 @@
     static int RestApiDelete(lua_State *state);
     static int GetOrthancConfiguration(lua_State *state);
 
-    void ApplyOnStoredInstance(const std::string& instanceId,
-                               const Json::Value& simplifiedDicom,
-                               const Json::Value& metadata,
-                               const DicomInstanceToStore& instance);
-
-    IServerCommand* ParseOperation(const std::string& operation,
-                                   const Json::Value& parameters);
+    size_t ParseOperation(LuaJobManager::Lock& lock,
+                          const std::string& operation,
+                          const Json::Value& parameters);
 
     void InitializeJob();
 
-    void SubmitJob(const std::string& description);
-
-    void OnStableResource(const ServerIndexChange& change);
+    void SubmitJob();
 
-    boost::recursive_mutex    mutex_;
-    LuaContext      lua_;
-    ServerContext&  context_;
+    boost::recursive_mutex   mutex_;
+    LuaContext               lua_;
+    ServerContext&           context_;
+    LuaJobManager            jobManager_;
+    State                    state_;
+    boost::thread            eventThread_;
+    SharedMessageQueue       pendingEvents_;
+
+    static void EventThread(LuaScripting* that);
+
+    void LoadGlobalConfiguration();
 
   public:
-    class Locker : public boost::noncopyable
+    class Lock : public boost::noncopyable
     {
     private:
-      LuaScripting& that_;
-      boost::recursive_mutex::scoped_lock lock_;
+      LuaScripting&                        that_;
+      boost::recursive_mutex::scoped_lock  lock_;
 
     public:
-      Locker(LuaScripting& that) : 
+      explicit Lock(LuaScripting& that) : 
         that_(that), 
         lock_(that.mutex_)
       {
@@ -92,16 +110,28 @@
     };
 
     LuaScripting(ServerContext& context);
-    
-    virtual void SignalStoredInstance(const std::string& publicId,
-                                      DicomInstanceToStore& instance,
-                                      const Json::Value& simplifiedTags);
+
+    ~LuaScripting();
+
+    void Start();
 
-    virtual void SignalChange(const ServerIndexChange& change);
+    void Stop();
+    
+    void SignalStoredInstance(const std::string& publicId,
+                              DicomInstanceToStore& instance,
+                              const Json::Value& simplifiedTags);
 
-    virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance,
-                                        const Json::Value& simplifiedTags);
+    void SignalChange(const ServerIndexChange& change);
+
+    bool FilterIncomingInstance(const DicomInstanceToStore& instance,
+                                const Json::Value& simplifiedTags);
 
     void Execute(const std::string& command);
+
+    void SignalJobSubmitted(const std::string& jobId);
+
+    void SignalJobSuccess(const std::string& jobId);
+
+    void SignalJobFailure(const std::string& jobId);
   };
 }
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,8 +35,9 @@
 #include "OrthancFindRequestHandler.h"
 
 #include "../Core/DicomFormat/DicomArray.h"
+#include "../Core/Lua/LuaFunctionCall.h"
 #include "../Core/Logging.h"
-#include "FromDcmtkBridge.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
 #include "OrthancInitialization.h"
 #include "Search/LookupResource.h"
 #include "ServerToolbox.h"
@@ -45,21 +47,336 @@
 
 namespace Orthanc
 {
+  static void GetChildren(std::list<std::string>& target,
+                          ServerIndex& index,
+                          const std::list<std::string>& source)
+  {
+    target.clear();
+
+    for (std::list<std::string>::const_iterator
+           it = source.begin(); it != source.end(); ++it)
+    {
+      std::list<std::string> tmp;
+      index.GetChildren(tmp, *it);
+      target.splice(target.end(), tmp);
+    }
+  }
+
+
+  static void StoreSetOfStrings(DicomMap& result,
+                                const DicomTag& tag,
+                                const std::set<std::string>& values)
+  {
+    bool isFirst = true;
+
+    std::string s;
+    for (std::set<std::string>::const_iterator
+           it = values.begin(); it != values.end(); ++it)
+    {
+      if (isFirst)
+      {
+        isFirst = false;
+      }
+      else
+      {
+        s += "\\";
+      }
+
+      s += *it;
+    }
+
+    result.SetValue(tag, s, false);
+  }
+
+
+  static void ExtractTagFromMainDicomTags(std::set<std::string>& target,
+                                          ServerIndex& index,
+                                          const DicomTag& tag,
+                                          const std::list<std::string>& resources,
+                                          ResourceType level)
+  {
+    for (std::list<std::string>::const_iterator
+           it = resources.begin(); it != resources.end(); ++it)
+    {
+      DicomMap tags;
+      if (index.GetMainDicomTags(tags, *it, level, level) &&
+          tags.HasTag(tag))
+      {
+        target.insert(tags.GetValue(tag).GetContent());
+      }
+    }
+  }
+
+
+  static bool ExtractMetadata(std::set<std::string>& target,
+                              ServerIndex& index,
+                              MetadataType metadata,
+                              const std::list<std::string>& resources)
+  {
+    for (std::list<std::string>::const_iterator
+           it = resources.begin(); it != resources.end(); ++it)
+    {
+      std::string value;
+      if (index.LookupMetadata(value, *it, metadata))
+      {
+        target.insert(value);
+      }
+      else
+      {
+        // This metadata is unavailable for some resource, give up
+        return false;
+      }
+    }
+
+    return true;
+  }  
+
+
+  static void ExtractTagFromInstancesOnDisk(std::set<std::string>& target,
+                                            ServerContext& context,
+                                            const DicomTag& tag,
+                                            const std::list<std::string>& instances)
+  {
+    // WARNING: This function is slow, as it reads the JSON file
+    // summarizing each instance of interest from the hard drive.
+
+    std::string formatted = tag.Format();
+
+    for (std::list<std::string>::const_iterator
+           it = instances.begin(); it != instances.end(); ++it)
+    {
+      Json::Value dicom;
+      context.ReadDicomAsJson(dicom, *it);
+
+      if (dicom.isMember(formatted))
+      {
+        const Json::Value& source = dicom[formatted];
+
+        if (source.type() == Json::objectValue &&
+            source.isMember("Type") &&
+            source.isMember("Value") &&
+            source["Type"].asString() == "String" &&
+            source["Value"].type() == Json::stringValue)
+        {
+          target.insert(source["Value"].asString());
+        }
+      }
+    }
+  }
+
+
+  static void ComputePatientCounters(DicomMap& result,
+                                     ServerIndex& index,
+                                     const std::string& patient,
+                                     const DicomMap& query)
+  {
+    std::list<std::string> studies;
+    index.GetChildren(studies, patient);
+
+    if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES))
+    {
+      result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES,
+                      boost::lexical_cast<std::string>(studies.size()), false);
+    }
+
+    if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) &&
+        !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
+    {
+      return;
+    }
+
+    std::list<std::string> series;
+    GetChildren(series, index, studies);
+    studies.clear();  // This information is useless below
+    
+    if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES))
+    {
+      result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES,
+                      boost::lexical_cast<std::string>(series.size()), false);
+    }
+
+    if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
+    {
+      return;
+    }
+
+    std::list<std::string> instances;
+    GetChildren(instances, index, series);
+
+    if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
+    {
+      result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES,
+                      boost::lexical_cast<std::string>(instances.size()), false);
+    }
+  }
+
+
+  static void ComputeStudyCounters(DicomMap& result,
+                                   ServerContext& context,
+                                   const std::string& study,
+                                   const DicomMap& query)
+  {
+    ServerIndex& index = context.GetIndex();
+
+    std::list<std::string> series;
+    index.GetChildren(series, study);
+    
+    if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES))
+    {
+      result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES,
+                      boost::lexical_cast<std::string>(series.size()), false);
+    }
+
+    if (query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
+    {
+      std::set<std::string> values;
+      ExtractTagFromMainDicomTags(values, index, DICOM_TAG_MODALITY, series, ResourceType_Series);
+      StoreSetOfStrings(result, DICOM_TAG_MODALITIES_IN_STUDY, values);
+    }
+
+    if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) &&
+        !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY))
+    {
+      return;
+    }
+
+    std::list<std::string> instances;
+    GetChildren(instances, index, series);
+
+    if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES))
+    {
+      result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES,
+                      boost::lexical_cast<std::string>(instances.size()), false);
+    }
+
+    if (query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY))
+    {
+      std::set<std::string> values;
+
+      if (ExtractMetadata(values, index, MetadataType_Instance_SopClassUid, instances))
+      {
+        // The metadata "SopClassUid" is available for each of these instances
+        StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
+      }
+      else if (Configuration::GetGlobalBoolParameter("AllowFindSopClassesInStudy", false))
+      {
+        ExtractTagFromInstancesOnDisk(values, context, DICOM_TAG_SOP_CLASS_UID, instances);
+        StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
+      }
+      else
+      {
+        result.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, "", false);
+        LOG(WARNING) << "The handling of \"SOP Classes in Study\" (0008,0062) "
+                     << "in C-FIND requests is disabled";
+      }
+    }
+  }
+
+
+  static void ComputeSeriesCounters(DicomMap& result,
+                                    ServerIndex& index,
+                                    const std::string& series,
+                                    const DicomMap& query)
+  {
+    std::list<std::string> instances;
+    index.GetChildren(instances, series);
+
+    if (query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES))
+    {
+      result.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES,
+                      boost::lexical_cast<std::string>(instances.size()), false);
+    }
+  }
+
+
+  static DicomMap* ComputeCounters(ServerContext& context,
+                                   const std::string& instanceId,
+                                   ResourceType level,
+                                   const DicomMap& query)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) &&
+            !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) &&
+            !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
+        {
+          return NULL;
+        }
+
+        break;
+
+      case ResourceType_Study:
+        if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) &&
+            !query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) &&
+            !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY) &&
+            !query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
+        {
+          return NULL;
+        }
+
+        break;
+
+      case ResourceType_Series:
+        if (!query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES))
+        {
+          return NULL;
+        }
+
+        break;
+
+      default:
+        return NULL;
+    }
+
+    std::string parent;
+    if (!context.GetIndex().LookupParent(parent, instanceId, level))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);  // The resource was deleted in between
+    }
+
+    std::auto_ptr<DicomMap> result(new DicomMap);
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        ComputePatientCounters(*result, context.GetIndex(), parent, query);
+        break;
+
+      case ResourceType_Study:
+        ComputeStudyCounters(*result, context, parent, query);
+        break;
+
+      case ResourceType_Series:
+        ComputeSeriesCounters(*result, context.GetIndex(), parent, query);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    return result.release();
+  }
+
+
   static void AddAnswer(DicomFindAnswers& answers,
                         const Json::Value& resource,
-                        const DicomArray& query)
+                        const DicomArray& query,
+                        const std::list<DicomTag>& sequencesToReturn,
+                        const DicomMap* counters)
   {
     DicomMap result;
 
     for (size_t i = 0; i < query.GetSize(); i++)
     {
-      // Fix issue 30 (QR response missing "Query/Retrieve Level" (008,0052))
       if (query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL)
       {
+        // Fix issue 30 on Google Code (QR response missing "Query/Retrieve Level" (008,0052))
         result.SetValue(query.GetElement(i).GetTag(), query.GetElement(i).GetValue());
       }
       else if (query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET)
       {
+        // Do not include the encoding, this is handled by class ParsedDicomFile
       }
       else
       {
@@ -68,56 +385,169 @@
         if (resource.isMember(tag))
         {
           value = resource.get(tag, Json::arrayValue).get("Value", "").asString();
-          result.SetValue(query.GetElement(i).GetTag(), value);
+          result.SetValue(query.GetElement(i).GetTag(), value, false);
         }
         else
         {
-          result.SetValue(query.GetElement(i).GetTag(), "");
+          result.SetValue(query.GetElement(i).GetTag(), "", false);
         }
       }
     }
 
-    if (result.GetSize() == 0)
+    if (counters != NULL)
+    {
+      DicomArray tmp(*counters);
+      for (size_t i = 0; i < tmp.GetSize(); i++)
+      {
+        result.SetValue(tmp.GetElement(i).GetTag(), tmp.GetElement(i).GetValue().GetContent(), false);
+      }
+    }
+
+    if (result.GetSize() == 0 &&
+        sequencesToReturn.empty())
     {
       LOG(WARNING) << "The C-FIND request does not return any DICOM tag";
     }
+    else if (sequencesToReturn.empty())
+    {
+      answers.Add(result);
+    }
     else
     {
-      answers.Add(result);
+      ParsedDicomFile dicom(result);
+
+      for (std::list<DicomTag>::const_iterator tag = sequencesToReturn.begin();
+           tag != sequencesToReturn.end(); ++tag)
+      {
+        const Json::Value& source = resource[tag->Format()];
+
+        if (source.type() == Json::objectValue &&
+            source.isMember("Type") &&
+            source.isMember("Value") &&
+            source["Type"].asString() == "Sequence" &&
+            source["Value"].type() == Json::arrayValue)
+        {
+          Json::Value content = Json::arrayValue;
+
+          for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++)
+          {
+            Json::Value item;
+            ServerToolbox::SimplifyTags(item, source["Value"][i], DicomToJsonFormat_Short);
+            content.append(item);
+          }
+
+          dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent);
+        }
+      }
+
+      answers.Add(dicom);
     }
   }
 
 
-  bool OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
+
+  bool OrthancFindRequestHandler::FilterQueryTag(std::string& value /* can be modified */,
+                                                 ResourceType level,
+                                                 const DicomTag& tag,
+                                                 ModalityManufacturer manufacturer)
+  {
+    // Whatever the manufacturer, remove the GenericGroupLength tags
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_7.2.html
+    // https://bitbucket.org/sjodogne/orthanc/issues/31/
+    if (tag.GetElement() == 0x0000)
+    {
+      return false;
+    }
+
+    switch (manufacturer)
+    {
+      case ModalityManufacturer_Vitrea:
+        // Following Denis Nesterov's mail on 2015-11-30
+        if (tag == DicomTag(0x5653, 0x0010))  // "PrivateCreator = Vital Images SW 3.4"
+        {
+          return false;
+        }
+
+        break;
+
+      default:
+        break;
+    }
+
+    return true;
+  }
+
+
+  bool OrthancFindRequestHandler::ApplyLuaFilter(DicomMap& target,
+                                                 const DicomMap& source,
+                                                 const std::string& remoteIp,
+                                                 const std::string& remoteAet,
+                                                 const std::string& calledAet,
+                                                 ModalityManufacturer manufacturer)
+  {
+    static const char* LUA_CALLBACK = "IncomingFindRequestFilter";
+    
+    LuaScripting::Lock lock(context_.GetLuaScripting());
+
+    if (!lock.GetLua().IsExistingFunction(LUA_CALLBACK))
+    {
+      return false;
+    }
+    else
+    {
+      Json::Value origin;
+      FormatOrigin(origin, remoteIp, remoteAet, calledAet, manufacturer);
+
+      LuaFunctionCall call(lock.GetLua(), LUA_CALLBACK);
+      call.PushDicom(source);
+      call.PushJson(origin);
+      FromDcmtkBridge::ExecuteToDicom(target, call);
+
+      return true;
+    }
+  }
+
+
+  OrthancFindRequestHandler::OrthancFindRequestHandler(ServerContext& context) :
+    context_(context),
+    maxResults_(0),
+    maxInstances_(0)
+  {
+  }
+
+
+  void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
                                          const DicomMap& input,
+                                         const std::list<DicomTag>& sequencesToReturn,
                                          const std::string& remoteIp,
-                                         const std::string& remoteAet)
+                                         const std::string& remoteAet,
+                                         const std::string& calledAet,
+                                         ModalityManufacturer manufacturer)
   {
     /**
-     * Ensure that the calling modality is known to Orthanc.
+     * Possibly apply the user-supplied Lua filter.
      **/
 
-    RemoteModalityParameters modality;
+    DicomMap lua;
+    const DicomMap* filteredInput = &input;
 
-    if (!Configuration::LookupDicomModalityUsingAETitle(modality, remoteAet))
+    if (ApplyLuaFilter(lua, input, remoteIp, remoteAet, calledAet, manufacturer))
     {
-      throw OrthancException(ErrorCode_UnknownModality);
+      filteredInput = &lua;
     }
 
-    // ModalityManufacturer manufacturer = modality.GetManufacturer();
-
-    bool caseSensitivePN = Configuration::GetGlobalBoolParameter("CaseSensitivePN", false);
-
 
     /**
      * Retrieve the query level.
      **/
 
-    const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+    assert(filteredInput != NULL);
+    const DicomValue* levelTmp = filteredInput->TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
     if (levelTmp == NULL ||
         levelTmp->IsNull() ||
         levelTmp->IsBinary())
     {
+      LOG(ERROR) << "C-FIND request without the tag 0008,0052 (QueryRetrieveLevel)";
       throw OrthancException(ErrorCode_BadRequest);
     }
 
@@ -132,7 +562,7 @@
     }
 
 
-    DicomArray query(input);
+    DicomArray query(*filteredInput);
     LOG(INFO) << "DICOM C-Find request at level: " << EnumerationToString(level);
 
     for (size_t i = 0; i < query.GetSize(); i++)
@@ -140,47 +570,66 @@
       if (!query.GetElement(i).GetValue().IsNull())
       {
         LOG(INFO) << "  " << query.GetElement(i).GetTag()
-                  << "  " << FromDcmtkBridge::GetName(query.GetElement(i).GetTag())
+                  << "  " << FromDcmtkBridge::GetTagName(query.GetElement(i))
                   << " = " << query.GetElement(i).GetValue().GetContent();
       }
     }
 
+    for (std::list<DicomTag>::const_iterator it = sequencesToReturn.begin();
+         it != sequencesToReturn.end(); ++it)
+    {
+      LOG(INFO) << "  (" << it->Format()
+                << ")  " << FromDcmtkBridge::GetTagName(*it, "")
+                << " : sequence tag whose content will be copied";
+    }
+
 
     /**
      * Build up the query object.
      **/
 
-    LookupResource finder(level);
+    LookupResource lookup(level);
+
+    const bool caseSensitivePN = Configuration::GetGlobalBoolParameter("CaseSensitivePN", false);
 
     for (size_t i = 0; i < query.GetSize(); i++)
     {
-      const DicomTag tag = query.GetElement(i).GetTag();
+      const DicomElement& element = query.GetElement(i);
+      const DicomTag tag = element.GetTag();
 
-      if (query.GetElement(i).GetValue().IsNull() ||
+      if (element.GetValue().IsNull() ||
           tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
           tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
       {
         continue;
       }
 
-      std::string value = query.GetElement(i).GetValue().GetContent();
+      std::string value = element.GetValue().GetContent();
       if (value.size() == 0)
       {
         // An empty string corresponds to a "*" wildcard constraint, so we ignore it
         continue;
       }
 
-      ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag);
+      if (FilterQueryTag(value, level, tag, manufacturer))
+      {
+        ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
 
-      // DICOM specifies that searches must be case sensitive, except
-      // for tags with a PN value representation
-      bool sensitive = true;
-      if (vr == ValueRepresentation_PatientName)
+        // DICOM specifies that searches must be case sensitive, except
+        // for tags with a PN value representation
+        bool sensitive = true;
+        if (vr == ValueRepresentation_PersonName)
+        {
+          sensitive = caseSensitivePN;
+        }
+
+        lookup.AddDicomConstraint(tag, value, sensitive);
+      }
+      else
       {
-        sensitive = caseSensitivePN;
+        LOG(INFO) << "Because of a patch for the manufacturer of the remote modality, " 
+                  << "ignoring constraint on tag (" << tag.Format() << ") " << FromDcmtkBridge::GetTagName(element);
       }
-
-      finder.AddDicomConstraint(tag, value, sensitive);
     }
 
 
@@ -188,36 +637,57 @@
      * Run the query.
      **/
 
-    size_t maxResults = (level == ResourceType_Instance) ? maxInstances_ : maxResults_;
+    size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_;
 
+    // TODO - Use ServerContext::Apply() at this point, in order to
+    // share the code with the "/tools/find" REST URI
     std::vector<std::string> resources, instances;
-    context_.GetIndex().FindCandidates(resources, instances, finder);
+    context_.GetIndex().FindCandidates(resources, instances, lookup);
+
+    LOG(INFO) << "Number of candidate resources after fast DB filtering: " << resources.size();
 
     assert(resources.size() == instances.size());
-    bool finished = true;
+    bool complete = true;
 
     for (size_t i = 0; i < instances.size(); i++)
     {
+      // TODO - Don't read the full JSON from the disk if only "main
+      // DICOM tags" are to be returned
       Json::Value dicom;
-      context_.ReadJson(dicom, instances[i]);
+      context_.ReadDicomAsJson(dicom, instances[i]);
       
-      if (finder.IsMatch(dicom))
+      if (lookup.IsMatch(dicom))
       {
-        if (maxResults != 0 &&
-            answers.GetSize() >= maxResults)
+        if (limit != 0 &&
+            answers.GetSize() >= limit)
         {
-          finished = false;
+          complete = false;
           break;
         }
         else
         {
-          AddAnswer(answers, dicom, query);
+          std::auto_ptr<DicomMap> counters(ComputeCounters(context_, instances[i], level, *filteredInput));
+          AddAnswer(answers, dicom, query, sequencesToReturn, counters.get());
         }
       }
     }
 
     LOG(INFO) << "Number of matching resources: " << answers.GetSize();
 
-    return finished;
+    answers.SetComplete(complete);
+  }
+
+
+  void OrthancFindRequestHandler::FormatOrigin(Json::Value& origin,
+                                               const std::string& remoteIp,
+                                               const std::string& remoteAet,
+                                               const std::string& calledAet,
+                                               ModalityManufacturer manufacturer)
+  {
+    origin = Json::objectValue;
+    origin["RemoteIp"] = remoteIp;
+    origin["RemoteAet"] = remoteAet;
+    origin["CalledAet"] = calledAet;
+    origin["Manufacturer"] = EnumerationToString(manufacturer);
   }
 }
--- a/OrthancServer/OrthancFindRequestHandler.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancFindRequestHandler.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -31,7 +32,7 @@
 
 #pragma once
 
-#include "DicomProtocol/IFindRequestHandler.h"
+#include "../Core/DicomNetworking/IFindRequestHandler.h"
 
 #include "ServerContext.h"
 
@@ -41,24 +42,34 @@
   {
   private:
     ServerContext& context_;
-    unsigned int maxResults_;
-    unsigned int maxInstances_;
+    unsigned int   maxResults_;
+    unsigned int   maxInstances_;
 
     bool HasReachedLimit(const DicomFindAnswers& answers,
                          ResourceType level) const;
 
+    bool FilterQueryTag(std::string& value /* can be modified */,
+                        ResourceType level,
+                        const DicomTag& tag,
+                        ModalityManufacturer manufacturer);
+
+    bool ApplyLuaFilter(DicomMap& target,
+                        const DicomMap& source,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet,
+                        ModalityManufacturer manufacturer);
+
   public:
-    OrthancFindRequestHandler(ServerContext& context) :
-      context_(context), 
-      maxResults_(0),
-      maxInstances_(0)
-    {
-    }
+    OrthancFindRequestHandler(ServerContext& context);
 
-    virtual bool Handle(DicomFindAnswers& answers,
+    virtual void Handle(DicomFindAnswers& answers,
                         const DicomMap& input,
+                        const std::list<DicomTag>& sequencesToReturn,
                         const std::string& remoteIp,
-                        const std::string& remoteAet);
+                        const std::string& remoteAet,
+                        const std::string& calledAet,
+                        ModalityManufacturer manufacturer);
 
     unsigned int GetMaxResults() const
     {
@@ -79,5 +90,11 @@
     {
       maxInstances_ = instances;
     }
+
+    static void FormatOrigin(Json::Value& origin,
+                             const std::string& remoteIp,
+                             const std::string& remoteAet,
+                             const std::string& calledAet,
+                             ModalityManufacturer manufacturer);
   };
 }
--- a/OrthancServer/OrthancHttpHandler.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancHttpHandler.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/OrthancHttpHandler.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancHttpHandler.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/OrthancInitialization.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancInitialization.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -31,7 +32,14 @@
 
 
 #include "PrecompiledHeadersServer.h"
+
+#if defined(_WIN32)
+// "Please include winsock2.h before windows.h"
+#  include <winsock2.h>
+#endif
+
 #include "OrthancInitialization.h"
+#include "ServerContext.h"
 
 #include "../Core/HttpClient.h"
 #include "../Core/Logging.h"
@@ -41,41 +49,25 @@
 
 #include "ServerEnumerations.h"
 #include "DatabaseWrapper.h"
-#include "FromDcmtkBridge.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
 
 #include <boost/lexical_cast.hpp>
 #include <boost/filesystem.hpp>
 #include <curl/curl.h>
-#include <boost/thread.hpp>
-
+#include <boost/thread/recursive_mutex.hpp>
 
-#if ORTHANC_SSL_ENABLED == 1
-// For OpenSSL initialization and finalization
-#include <openssl/conf.h>
-#include <openssl/engine.h>
-#include <openssl/err.h>
-#include <openssl/evp.h>
-#include <openssl/ssl.h>
-#endif
+#include <dcmtk/dcmnet/dul.h>   // For dcmDisableGethostbyaddr()
 
 
-#if ORTHANC_JPEG_ENABLED == 1
-#include <dcmtk/dcmjpeg/djdecode.h>
-#endif
-
-
-#if ORTHANC_JPEG_LOSSLESS_ENABLED == 1
-#include <dcmtk/dcmjpls/djdecode.h>
-#endif
-
 
 namespace Orthanc
 {
-  static boost::mutex globalMutex_;
+  static boost::recursive_mutex globalMutex_;
   static Json::Value configuration_;
   static boost::filesystem::path defaultDirectory_;
   static std::string configurationAbsolutePath_;
   static FontRegistry fontRegistry_;
+  static const char* configurationFileArg_ = NULL;
 
 
   static std::string GetGlobalStringParameterInternal(const std::string& parameter,
@@ -123,7 +115,8 @@
 
 
 
-  static void AddFileToConfiguration(const boost::filesystem::path& path)
+  static void AddFileToConfiguration(Json::Value& target,
+                                     const boost::filesystem::path& path)
   {
     LOG(WARNING) << "Reading the configuration from: " << path;
 
@@ -131,7 +124,7 @@
 
     {
       std::string content;
-      Toolbox::ReadFile(content, path.string());
+      SystemToolbox::ReadFile(content, path.string());
 
       Json::Value tmp;
       Json::Reader reader;
@@ -145,30 +138,31 @@
       Toolbox::CopyJsonWithoutComments(config, tmp);
     }
 
-    if (configuration_.size() == 0)
+    if (target.size() == 0)
     {
-      configuration_ = config;
+      target = config;
     }
     else
     {
       Json::Value::Members members = config.getMemberNames();
       for (Json::Value::ArrayIndex i = 0; i < members.size(); i++)
       {
-        if (configuration_.isMember(members[i]))
+        if (target.isMember(members[i]))
         {
           LOG(ERROR) << "The configuration section \"" << members[i] << "\" is defined in 2 different configuration files";
           throw OrthancException(ErrorCode_BadFileFormat);          
         }
         else
         {
-          configuration_[members[i]] = config[members[i]];
+          target[members[i]] = config[members[i]];
         }
       }
     }
   }
 
 
-  static void ScanFolderForConfiguration(const char* folder)
+  static void ScanFolderForConfiguration(Json::Value& target,
+                                         const char* folder)
   {
     using namespace boost::filesystem;
 
@@ -186,19 +180,17 @@
 
         if (extension == ".json")
         {
-          AddFileToConfiguration(it->path().string());
+          AddFileToConfiguration(target, it->path().string());
         }
       }
     }
   }
 
 
-  static void ReadGlobalConfiguration(const char* configurationFile)
+  static void ReadConfiguration(Json::Value& target,
+                                const char* configurationFile)
   {
-    // Prepare the default configuration
-    defaultDirectory_ = boost::filesystem::current_path();
-    configuration_ = Json::objectValue;
-    configurationAbsolutePath_ = "";
+    target = Json::objectValue;
 
     if (configurationFile)
     {
@@ -210,15 +202,11 @@
       
       if (boost::filesystem::is_directory(configurationFile))
       {
-        defaultDirectory_ = boost::filesystem::path(configurationFile);
-        configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).parent_path().string();
-        ScanFolderForConfiguration(configurationFile);
+        ScanFolderForConfiguration(target, configurationFile);
       }
       else
       {
-        defaultDirectory_ = boost::filesystem::path(configurationFile).parent_path();
-        configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).string();
-        AddFileToConfiguration(configurationFile);
+        AddFileToConfiguration(target, configurationFile);
       }
     }
     else
@@ -235,14 +223,73 @@
       boost::filesystem::path p = ORTHANC_PATH;
       p /= "Resources";
       p /= "Configuration.json";
-      configurationAbsolutePath_ = boost::filesystem::absolute(p).string();
+
+      AddFileToConfiguration(target, p);
+#endif
+    }
+  }
+
+
+
+  static void ReadGlobalConfiguration(const char* configurationFile)
+  {
+    // Read the content of the configuration
+    configurationFileArg_ = configurationFile;
+    ReadConfiguration(configuration_, configurationFile);
+
+    // Adapt the paths to the configurations
+    defaultDirectory_ = boost::filesystem::current_path();
+    configurationAbsolutePath_ = "";
 
-      AddFileToConfiguration(p);      
+    if (configurationFile)
+    {
+      if (boost::filesystem::is_directory(configurationFile))
+      {
+        defaultDirectory_ = boost::filesystem::path(configurationFile);
+        configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).parent_path().string();
+      }
+      else
+      {
+        defaultDirectory_ = boost::filesystem::path(configurationFile).parent_path();
+        configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).string();
+      }
+    }
+    else
+    {
+#if ORTHANC_STANDALONE != 1
+      // In a non-standalone build, we use the
+      // "Resources/Configuration.json" from the Orthanc source code
+
+      boost::filesystem::path p = ORTHANC_PATH;
+      p /= "Resources";
+      p /= "Configuration.json";
+      configurationAbsolutePath_ = boost::filesystem::absolute(p).string();
 #endif
     }
   }
 
 
+  static void ValidateGlobalConfiguration()
+  {
+    std::set<std::string> ids;
+
+    Configuration::GetListOfOrthancPeers(ids);
+    for (std::set<std::string>::const_iterator it = ids.begin(); it != ids.end(); ++it)
+    {
+      WebServiceParameters peer;
+      Configuration::GetOrthancPeer(peer, *it);
+      peer.CheckClientCertificate();
+    }
+
+    Configuration::GetListOfDicomModalities(ids);
+    for (std::set<std::string>::const_iterator it = ids.begin(); it != ids.end(); ++it)
+    {
+      RemoteModalityParameters modality;
+      Configuration::GetDicomModalityUsingSymbolicName(modality, *it);
+    }
+  }
+
+
   static void RegisterUserMetadata()
   {
     if (configuration_.isMember("UserMetadata"))
@@ -252,24 +299,26 @@
       Json::Value::Members members = parameter.getMemberNames();
       for (size_t i = 0; i < members.size(); i++)
       {
-        std::string info = "\"" + members[i] + "\" = " + parameter[members[i]].toStyledString();
-        LOG(INFO) << "Registering user-defined metadata: " << info;
+        const std::string& name = members[i];
 
-        if (!parameter[members[i]].asBool())
+        if (!parameter[name].isInt())
         {
-          LOG(ERROR) << "Not a number in this user-defined metadata: " << info;
+          LOG(ERROR) << "Not a number in this user-defined metadata: " << name;
           throw OrthancException(ErrorCode_BadParameterType);
         }
 
-        int metadata = parameter[members[i]].asInt();
+        int metadata = parameter[name].asInt();        
+
+        LOG(INFO) << "Registering user-defined metadata: " << name << " (index " 
+                  << metadata << ")";
 
         try
         {
-          RegisterUserMetadata(metadata, members[i]);
+          RegisterUserMetadata(metadata, name);
         }
         catch (OrthancException&)
         {
-          LOG(ERROR) << "Cannot register this user-defined metadata: " << info;
+          LOG(ERROR) << "Cannot register this user-defined metadata: " << name;
           throw;
         }
       }
@@ -286,24 +335,39 @@
       Json::Value::Members members = parameter.getMemberNames();
       for (size_t i = 0; i < members.size(); i++)
       {
-        std::string info = "\"" + members[i] + "\" = " + parameter[members[i]].toStyledString();
-        LOG(INFO) << "Registering user-defined attachment type: " << info;
+        const std::string& name = members[i];
+        std::string mime = "application/octet-stream";
+
+        const Json::Value& value = parameter[name];
+        int contentType;
 
-        if (!parameter[members[i]].asBool())
+        if (value.isArray() &&
+            value.size() == 2 &&
+            value[0].isInt() &&
+            value[1].isString())
         {
-          LOG(ERROR) << "Not a number in this user-defined attachment type: " << info;
+          contentType = value[0].asInt();
+          mime = value[1].asString();
+        }
+        else if (value.isInt())
+        {
+          contentType = value.asInt();
+        }
+        else
+        {
+          LOG(ERROR) << "Not a number in this user-defined attachment type: " << name;
           throw OrthancException(ErrorCode_BadParameterType);
         }
 
-        int contentType = parameter[members[i]].asInt();
+        LOG(INFO) << "Registering user-defined attachment type: " << name << " (index " 
+                  << contentType << ") with MIME type \"" << mime << "\"";
 
         try
         {
-          RegisterUserContentType(contentType, members[i]);
+          RegisterUserContentType(contentType, name, mime);
         }
         catch (OrthancException&)
         {
-          LOG(ERROR) << "Cannot register this user-defined attachment type: " << info;
           throw;
         }
       }
@@ -311,86 +375,156 @@
   }
 
 
+  static void LoadCustomDictionary(const Json::Value& configuration)
+  {
+    if (configuration.type() != Json::objectValue ||
+        !configuration.isMember("Dictionary") ||
+        configuration["Dictionary"].type() != Json::objectValue)
+    {
+      return;
+    }
+
+    Json::Value::Members tags(configuration["Dictionary"].getMemberNames());
+
+    for (Json::Value::ArrayIndex i = 0; i < tags.size(); i++)
+    {
+      const Json::Value& content = configuration["Dictionary"][tags[i]];
+      if (content.type() != Json::arrayValue ||
+          content.size() < 2 ||
+          content.size() > 5 ||
+          content[0].type() != Json::stringValue ||
+          content[1].type() != Json::stringValue ||
+          (content.size() >= 3 && content[2].type() != Json::intValue) ||
+          (content.size() >= 4 && content[3].type() != Json::intValue) ||
+          (content.size() >= 5 && content[4].type() != Json::stringValue))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      DicomTag tag(FromDcmtkBridge::ParseTag(tags[i]));
+      ValueRepresentation vr = StringToValueRepresentation(content[0].asString(), true);
+      std::string name = content[1].asString();
+      unsigned int minMultiplicity = (content.size() >= 2) ? content[2].asUInt() : 1;
+      unsigned int maxMultiplicity = (content.size() >= 3) ? content[3].asUInt() : 1;
+      std::string privateCreator = (content.size() >= 4) ? content[4].asString() : "";
+
+      FromDcmtkBridge::RegisterDictionaryTag(tag, vr, name, minMultiplicity, maxMultiplicity, privateCreator);
+    }
+  }
+
+
+  static void ConfigurePkcs11(const Json::Value& config)
+  {
+    if (config.type() != Json::objectValue ||
+        !config.isMember("Module") ||
+        config["Module"].type() != Json::stringValue)
+    {
+      LOG(ERROR) << "No path to the PKCS#11 module (DLL or .so) is provided for HTTPS client authentication";
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    std::string pin;
+    if (config.isMember("Pin"))
+    {
+      if (config["Pin"].type() == Json::stringValue)
+      {
+        pin = config["Pin"].asString();
+      }
+      else
+      {
+        LOG(ERROR) << "The PIN number in the PKCS#11 configuration must be a string";
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+
+    bool verbose = false;
+    if (config.isMember("Verbose"))
+    {
+      if (config["Verbose"].type() == Json::booleanValue)
+      {
+        verbose = config["Verbose"].asBool();
+      }
+      else
+      {
+        LOG(ERROR) << "The Verbose option in the PKCS#11 configuration must be a Boolean";
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+
+    HttpClient::InitializePkcs11(config["Module"].asString(), pin, verbose);
+  }
+
+
 
   void OrthancInitialize(const char* configurationFile)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    boost::recursive_mutex::scoped_lock lock(globalMutex_);
 
-#if ORTHANC_SSL_ENABLED == 1
-    // https://wiki.openssl.org/index.php/Library_Initialization
-    SSL_library_init();
-    SSL_load_error_strings();
-    OpenSSL_add_all_algorithms();
-    ERR_load_crypto_strings();
-
-    curl_global_init(CURL_GLOBAL_ALL);
-#else
-    curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_SSL);
-#endif
+    Toolbox::InitializeOpenSsl();
 
     InitializeServerEnumerations();
 
     // Read the user-provided configuration
     ReadGlobalConfiguration(configurationFile);
+    ValidateGlobalConfiguration();
 
-    HttpClient::GlobalInitialize(GetGlobalBoolParameterInternal("HttpsVerifyPeers", true),
-                                 GetGlobalStringParameterInternal("HttpsCACertificates", ""));
+    if (configuration_.isMember("Locale"))
+    {
+      std::string locale = GetGlobalStringParameterInternal("Locale", "");
+      Toolbox::InitializeGlobalLocale(configuration_["Locale"].asCString());
+    }
+    else
+    {
+      Toolbox::InitializeGlobalLocale(NULL);
+    }
+
+    if (configuration_.isMember("DefaultEncoding"))
+    {
+      std::string encoding = GetGlobalStringParameterInternal("DefaultEncoding", "");
+      SetDefaultDicomEncoding(StringToEncoding(encoding.c_str()));
+    }
+    else
+    {
+      SetDefaultDicomEncoding(ORTHANC_DEFAULT_DICOM_ENCODING);
+    }
+
+    if (configuration_.isMember("Pkcs11"))
+    {
+      ConfigurePkcs11(configuration_["Pkcs11"]);
+    }
+
+    HttpClient::GlobalInitialize();
 
     RegisterUserMetadata();
     RegisterUserContentType();
 
-    FromDcmtkBridge::InitializeDictionary();
+    FromDcmtkBridge::InitializeDictionary(GetGlobalBoolParameterInternal("LoadPrivateDictionary", true));
+    LoadCustomDictionary(configuration_);
 
-#if ORTHANC_JPEG_LOSSLESS_ENABLED == 1
-    LOG(WARNING) << "Registering JPEG Lossless codecs";
-    DJLSDecoderRegistration::registerCodecs();    
-#endif
-
-#if ORTHANC_JPEG_ENABLED == 1
-    LOG(WARNING) << "Registering JPEG codecs";
-    DJDecoderRegistration::registerCodecs(); 
-#endif
+    FromDcmtkBridge::InitializeCodecs();
 
     fontRegistry_.AddFromResource(EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
+
+    /* Disable "gethostbyaddr" (which results in memory leaks) and use raw IP addresses */
+    dcmDisableGethostbyaddr.set(OFTrue);
   }
 
 
 
   void OrthancFinalize()
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    boost::recursive_mutex::scoped_lock lock(globalMutex_);
     HttpClient::GlobalFinalize();
-
-#if ORTHANC_JPEG_LOSSLESS_ENABLED == 1
-    // Unregister JPEG-LS codecs
-    DJLSDecoderRegistration::cleanup();
-#endif
-
-#if ORTHANC_JPEG_ENABLED == 1
-    // Unregister JPEG codecs
-    DJDecoderRegistration::cleanup();
-#endif
-
-    curl_global_cleanup();
-
-#if ORTHANC_SSL_ENABLED == 1
-    // Finalize OpenSSL
-    // https://wiki.openssl.org/index.php/Library_Initialization#Cleanup
-    FIPS_mode_set(0);
-    ENGINE_cleanup();
-    CONF_modules_unload(1);
-    EVP_cleanup();
-    CRYPTO_cleanup_all_ex_data();
-    ERR_remove_state(0);
-    ERR_free_strings();
-#endif
+    FromDcmtkBridge::FinalizeCodecs();
+    Toolbox::FinalizeOpenSsl();
+    Toolbox::FinalizeGlobalLocale();
   }
 
 
   std::string Configuration::GetGlobalStringParameter(const std::string& parameter,
                                                       const std::string& defaultValue)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    boost::recursive_mutex::scoped_lock lock(globalMutex_);
     return GetGlobalStringParameterInternal(parameter, defaultValue);
   }
 
@@ -398,7 +532,7 @@
   int Configuration::GetGlobalIntegerParameter(const std::string& parameter,
                                                int defaultValue)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    boost::recursive_mutex::scoped_lock lock(globalMutex_);
 
     if (configuration_.isMember(parameter))
     {
@@ -419,10 +553,27 @@
   }
 
 
+  unsigned int Configuration::GetGlobalUnsignedIntegerParameter(const std::string& parameter,
+                                                                unsigned int defaultValue)
+  {
+    int v = GetGlobalIntegerParameter(parameter, defaultValue);
+
+    if (v < 0)
+    {
+      LOG(ERROR) << "The configuration option \"" << parameter << "\" must be a positive integer";
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return static_cast<unsigned int>(v);
+    }
+  }
+
+
   bool Configuration::GetGlobalBoolParameter(const std::string& parameter,
                                              bool defaultValue)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    boost::recursive_mutex::scoped_lock lock(globalMutex_);
     return GetGlobalBoolParameterInternal(parameter, defaultValue);
   }
 
@@ -430,7 +581,7 @@
   void Configuration::GetDicomModalityUsingSymbolicName(RemoteModalityParameters& modality,
                                                         const std::string& name)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    boost::recursive_mutex::scoped_lock lock(globalMutex_);
 
     if (!configuration_.isMember("DicomModalities"))
     {
@@ -448,7 +599,7 @@
 
     try
     {
-      modality.FromJson(modalities[name]);
+      modality.Unserialize(modalities[name]);
     }
     catch (OrthancException&)
     {
@@ -459,16 +610,14 @@
   }
 
 
-
-  void Configuration::GetOrthancPeer(OrthancPeerParameters& peer,
+  bool Configuration::GetOrthancPeer(WebServiceParameters& peer,
                                      const std::string& name)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    boost::recursive_mutex::scoped_lock lock(globalMutex_);
 
     if (!configuration_.isMember("OrthancPeers"))
     {
-      LOG(ERROR) << "No peer with symbolic name: " << name;
-      throw OrthancException(ErrorCode_InexistentItem);
+      return false;
     }
 
     try
@@ -477,11 +626,13 @@
       if (modalities.type() != Json::objectValue ||
           !modalities.isMember(name))
       {
-        LOG(ERROR) << "No peer with symbolic name: " << name;
-        throw OrthancException(ErrorCode_InexistentItem);
+        return false;
       }
-
-      peer.FromJson(modalities[name]);
+      else
+      {
+        peer.Unserialize(modalities[name]);
+        return true;
+      }
     }
     catch (OrthancException&)
     {
@@ -496,7 +647,7 @@
                        const char* parameter,
                        bool onlyAlphanumeric)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    boost::recursive_mutex::scoped_lock lock(globalMutex_);
 
     target.clear();
   
@@ -535,6 +686,8 @@
 
   void Configuration::GetListOfDicomModalities(std::set<std::string>& target)
   {
+    target.clear();
+
     if (!ReadKeys(target, "DicomModalities", true))
     {
       LOG(ERROR) << "Only alphanumeric and dash characters are allowed in the names of the modalities";
@@ -545,6 +698,8 @@
 
   void Configuration::GetListOfOrthancPeers(std::set<std::string>& target)
   {
+    target.clear();
+
     if (!ReadKeys(target, "OrthancPeers", true))
     {
       LOG(ERROR) << "Only alphanumeric and dash characters are allowed in the names of Orthanc peers";
@@ -556,7 +711,7 @@
 
   void Configuration::SetupRegisteredUsers(MongooseServer& httpServer)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    boost::recursive_mutex::scoped_lock lock(globalMutex_);
 
     httpServer.ClearUsers();
 
@@ -610,7 +765,7 @@
 
   std::string Configuration::InterpretStringParameterAsPath(const std::string& parameter)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    boost::recursive_mutex::scoped_lock lock(globalMutex_);
     return InterpretRelativePath(defaultDirectory_.string(), parameter);
   }
 
@@ -618,7 +773,7 @@
   void Configuration::GetGlobalListOfStringsParameter(std::list<std::string>& target,
                                                       const std::string& key)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    boost::recursive_mutex::scoped_lock lock(globalMutex_);
 
     target.clear();
   
@@ -688,10 +843,30 @@
   }
 
 
-  bool Configuration::IsKnownAETitle(const std::string& aet)
+  bool Configuration::IsKnownAETitle(const std::string& aet,
+                                     const std::string& ip)
   {
     RemoteModalityParameters modality;
-    return LookupDicomModalityUsingAETitle(modality, aet);
+    
+    if (!LookupDicomModalityUsingAETitle(modality, aet))
+    {
+      LOG(WARNING) << "Modality \"" << aet
+                   << "\" is not listed in the \"DicomModalities\" configuration option";
+      return false;
+    }
+    else if (!Configuration::GetGlobalBoolParameter("DicomCheckModalityHost", false) ||
+             ip == modality.GetHost())
+    {
+      return true;
+    }
+    else
+    {
+      LOG(WARNING) << "Forbidding access from AET \"" << aet
+                   << "\" given its hostname (" << ip << ") does not match "
+                   << "the \"DicomModalities\" configuration option ("
+                   << modality.GetHost() << " was expected)";
+      return false;
+    }
   }
 
 
@@ -720,96 +895,140 @@
   }
 
 
-  void Configuration::UpdateModality(const std::string& symbolicName,
+  void Configuration::UpdateModality(ServerContext& context,
+                                     const std::string& symbolicName,
                                      const RemoteModalityParameters& modality)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    {
+      boost::recursive_mutex::scoped_lock lock(globalMutex_);
+
+      if (!configuration_.isMember("DicomModalities"))
+      {
+        configuration_["DicomModalities"] = Json::objectValue;
+      }
 
-    if (!configuration_.isMember("DicomModalities"))
-    {
-      configuration_["DicomModalities"] = Json::objectValue;
+      Json::Value& modalities = configuration_["DicomModalities"];
+      if (modalities.type() != Json::objectValue)
+      {
+        LOG(ERROR) << "Bad file format for modality: " << symbolicName;
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      modalities.removeMember(symbolicName);
+
+      Json::Value v;
+      modality.Serialize(v, true /* force advanced format */);
+      modalities[symbolicName] = v;
     }
 
-    Json::Value& modalities = configuration_["DicomModalities"];
-    if (modalities.type() != Json::objectValue)
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (context.HasPlugins())
     {
-      LOG(ERROR) << "Bad file format for modality: " << symbolicName;
-      throw OrthancException(ErrorCode_BadFileFormat);
+      context.GetPlugins().SignalUpdatedModalities();
     }
-
-    modalities.removeMember(symbolicName);
-
-    Json::Value v;
-    modality.ToJson(v);
-    modalities[symbolicName] = v;
+#endif
   }
   
 
-  void Configuration::RemoveModality(const std::string& symbolicName)
+  void Configuration::RemoveModality(ServerContext& context,
+                                     const std::string& symbolicName)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    {
+      boost::recursive_mutex::scoped_lock lock(globalMutex_);
 
-    if (!configuration_.isMember("DicomModalities"))
-    {
-      LOG(ERROR) << "No modality with symbolic name: " << symbolicName;
-      throw OrthancException(ErrorCode_BadFileFormat);
+      if (!configuration_.isMember("DicomModalities"))
+      {
+        LOG(ERROR) << "No modality with symbolic name: " << symbolicName;
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      Json::Value& modalities = configuration_["DicomModalities"];
+      if (modalities.type() != Json::objectValue)
+      {
+        LOG(ERROR) << "Bad file format for the \"DicomModalities\" configuration section";
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      modalities.removeMember(symbolicName.c_str());
     }
 
-    Json::Value& modalities = configuration_["DicomModalities"];
-    if (modalities.type() != Json::objectValue)
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (context.HasPlugins())
     {
-      LOG(ERROR) << "Bad file format for the \"DicomModalities\" configuration section";
-      throw OrthancException(ErrorCode_BadFileFormat);
+      context.GetPlugins().SignalUpdatedModalities();
     }
-
-    modalities.removeMember(symbolicName.c_str());
+#endif
   }
 
 
-  void Configuration::UpdatePeer(const std::string& symbolicName,
-                                 const OrthancPeerParameters& peer)
+  void Configuration::UpdatePeer(ServerContext& context,
+                                 const std::string& symbolicName,
+                                 const WebServiceParameters& peer)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    peer.CheckClientCertificate();
+
+    {
+      boost::recursive_mutex::scoped_lock lock(globalMutex_);
+
+      if (!configuration_.isMember("OrthancPeers"))
+      {
+        LOG(ERROR) << "No peer with symbolic name: " << symbolicName;
+        configuration_["OrthancPeers"] = Json::objectValue;
+      }
 
-    if (!configuration_.isMember("OrthancPeers"))
-    {
-      LOG(ERROR) << "No peer with symbolic name: " << symbolicName;
-      configuration_["OrthancPeers"] = Json::objectValue;
+      Json::Value& peers = configuration_["OrthancPeers"];
+      if (peers.type() != Json::objectValue)
+      {
+        LOG(ERROR) << "Bad file format for the \"OrthancPeers\" configuration section";
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      peers.removeMember(symbolicName);
+
+      Json::Value v;
+      peer.Serialize(v, 
+                     false /* use simple format if possible */, 
+                     true  /* include passwords */);
+      peers[symbolicName] = v;
     }
 
-    Json::Value& peers = configuration_["OrthancPeers"];
-    if (peers.type() != Json::objectValue)
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (context.HasPlugins())
     {
-      LOG(ERROR) << "Bad file format for the \"OrthancPeers\" configuration section";
-      throw OrthancException(ErrorCode_BadFileFormat);
+      context.GetPlugins().SignalUpdatedPeers();
     }
-
-    peers.removeMember(symbolicName);
-
-    Json::Value v;
-    peer.ToJson(v);
-    peers[symbolicName] = v;
+#endif
   }
   
 
-  void Configuration::RemovePeer(const std::string& symbolicName)
+  void Configuration::RemovePeer(ServerContext& context,
+                                 const std::string& symbolicName)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    {
+      boost::recursive_mutex::scoped_lock lock(globalMutex_);
 
-    if (!configuration_.isMember("OrthancPeers"))
-    {
-      LOG(ERROR) << "No peer with symbolic name: " << symbolicName;
-      throw OrthancException(ErrorCode_BadFileFormat);
+      if (!configuration_.isMember("OrthancPeers"))
+      {
+        LOG(ERROR) << "No peer with symbolic name: " << symbolicName;
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      Json::Value& peers = configuration_["OrthancPeers"];
+      if (peers.type() != Json::objectValue)
+      {
+        LOG(ERROR) << "Bad file format for the \"OrthancPeers\" configuration section";
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      peers.removeMember(symbolicName.c_str());
     }
 
-    Json::Value& peers = configuration_["OrthancPeers"];
-    if (peers.type() != Json::objectValue)
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (context.HasPlugins())
     {
-      LOG(ERROR) << "Bad file format for the \"OrthancPeers\" configuration section";
-      throw OrthancException(ErrorCode_BadFileFormat);
+      context.GetPlugins().SignalUpdatedPeers();
     }
-
-    peers.removeMember(symbolicName.c_str());
+#endif
   }
 
 
@@ -834,7 +1053,7 @@
     {
       boost::filesystem::create_directories(indexDirectory);
     }
-    catch (boost::filesystem::filesystem_error)
+    catch (boost::filesystem::filesystem_error&)
     {
     }
 
@@ -926,7 +1145,7 @@
 
   void Configuration::GetConfiguration(Json::Value& result)
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    boost::recursive_mutex::scoped_lock lock(globalMutex_);
     result = configuration_;
   }
 
@@ -945,4 +1164,33 @@
   {
     return fontRegistry_;
   }
+
+
+  void Configuration::SetDefaultEncoding(Encoding encoding)
+  {
+    SetDefaultDicomEncoding(encoding);
+
+    {
+      // Propagate the encoding to the configuration file that is
+      // stored in memory
+      boost::recursive_mutex::scoped_lock lock(globalMutex_);
+      configuration_["DefaultEncoding"] = EnumerationToString(encoding);
+    }
+  }
+
+
+  bool Configuration::HasConfigurationChanged()
+  {
+    Json::Value starting;
+    GetConfiguration(starting);
+
+    Json::Value current;
+    ReadConfiguration(current, configurationFileArg_);
+
+    Json::FastWriter writer;
+    std::string a = writer.write(starting);
+    std::string b = writer.write(current);
+
+    return a != b;
+  }
 }
--- a/OrthancServer/OrthancInitialization.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancInitialization.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -36,22 +37,30 @@
 #include <set>
 #include <json/json.h>
 #include <stdint.h>
+
+#include "../Core/FileStorage/IStorageArea.h"
 #include "../Core/HttpServer/MongooseServer.h"
-#include "DicomProtocol/RemoteModalityParameters.h"
+#include "../Core/Images/FontRegistry.h"
+#include "../Core/WebServiceParameters.h"
+#include "../Core/DicomNetworking/RemoteModalityParameters.h"
+
+#include "IDatabaseWrapper.h"
 #include "ServerEnumerations.h"
-#include "OrthancPeerParameters.h"
-#include "IDatabaseWrapper.h"
-#include "../Core/FileStorage/IStorageArea.h"
-#include "../Core/Images/FontRegistry.h"
+
 
 namespace Orthanc
 {
+  class ServerContext;
+
   void OrthancInitialize(const char* configurationFile = NULL);
 
   void OrthancFinalize();
 
   class Configuration
   {
+  private:
+    Configuration();  // Forbidden, this is a static class
+
   public:
     static std::string GetGlobalStringParameter(const std::string& parameter,
                                                 const std::string& defaultValue);
@@ -59,6 +68,9 @@
     static int GetGlobalIntegerParameter(const std::string& parameter,
                                          int defaultValue);
 
+    static unsigned int GetGlobalUnsignedIntegerParameter(const std::string& parameter,
+                                                          unsigned int defaultValue);
+
     static bool GetGlobalBoolParameter(const std::string& parameter,
                                        bool defaultValue);
 
@@ -68,7 +80,7 @@
     static bool LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality,
                                                 const std::string& aet);
 
-    static void GetOrthancPeer(OrthancPeerParameters& peer,
+    static bool GetOrthancPeer(WebServiceParameters& peer,
                                const std::string& name);
 
     static void GetListOfDicomModalities(std::set<std::string>& target);
@@ -85,7 +97,8 @@
     static void GetGlobalListOfStringsParameter(std::list<std::string>& target,
                                                 const std::string& key);
 
-    static bool IsKnownAETitle(const std::string& aet);
+    static bool IsKnownAETitle(const std::string& aet,
+                               const std::string& ip);
 
     static bool IsSameAETitle(const std::string& aet1,
                               const std::string& aet2);
@@ -94,15 +107,19 @@
 
     static RemoteModalityParameters GetModalityUsingAet(const std::string& aet);
 
-    static void UpdateModality(const std::string& symbolicName,
+    static void UpdateModality(ServerContext& context,
+                               const std::string& symbolicName,
                                const RemoteModalityParameters& modality);
 
-    static void RemoveModality(const std::string& symbolicName);
+    static void RemoveModality(ServerContext& context,
+                               const std::string& symbolicName);
 
-    static void UpdatePeer(const std::string& symbolicName,
-                           const OrthancPeerParameters& peer);
+    static void UpdatePeer(ServerContext& context,
+                           const std::string& symbolicName,
+                           const WebServiceParameters& peer);
 
-    static void RemovePeer(const std::string& symbolicName);
+    static void RemovePeer(ServerContext& context,
+                           const std::string& symbolicName);
 
     static const std::string& GetConfigurationAbsolutePath();
 
@@ -115,5 +132,9 @@
     static void FormatConfiguration(std::string& result);
 
     static const FontRegistry& GetFontRegistry();
+
+    static void SetDefaultEncoding(Encoding encoding);
+
+    static bool HasConfigurationChanged();
   };
 }
--- a/OrthancServer/OrthancMoveRequestHandler.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,9 +35,10 @@
 #include "OrthancMoveRequestHandler.h"
 
 #include "OrthancInitialization.h"
-#include "FromDcmtkBridge.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/DicomFormat/DicomArray.h"
 #include "../Core/Logging.h"
+#include "ServerJobs/DicomModalityStoreJob.h"
 
 namespace Orthanc
 {
@@ -44,7 +46,7 @@
   {
     // Anonymous namespace to avoid clashes between compilation modules
 
-    class OrthancMoveRequestIterator : public IMoveRequestIterator
+    class SynchronousMove : public IMoveRequestIterator
     {
     private:
       ServerContext& context_;
@@ -52,16 +54,23 @@
       std::vector<std::string> instances_;
       size_t position_;
       RemoteModalityParameters remote_;
+      std::string originatorAet_;
+      uint16_t originatorId_;
+      std::auto_ptr<DicomUserConnection> connection_;
 
     public:
-      OrthancMoveRequestIterator(ServerContext& context,
-                                 const std::string& aet,
-                                 const std::string& publicId) :
+      SynchronousMove(ServerContext& context,
+                      const std::string& targetAet,
+                      const std::string& publicId,
+                      const std::string& originatorAet,
+                      uint16_t originatorId) :
         context_(context),
         localAet_(context.GetDefaultLocalApplicationEntityTitle()),
-        position_(0)
+        position_(0),
+        originatorAet_(originatorAet),
+        originatorId_(originatorId)
       {
-        LOG(INFO) << "Sending resource " << publicId << " to modality \"" << aet << "\"";
+        LOG(INFO) << "Sending resource " << publicId << " to modality \"" << targetAet << "\"";
 
         std::list<std::string> tmp;
         context_.GetIndex().GetChildInstances(tmp, publicId);
@@ -72,7 +81,7 @@
           instances_.push_back(*it);
         }
 
-        remote_ = Configuration::GetModalityUsingAet(aet);
+        remote_ = Configuration::GetModalityUsingAet(targetAet);
       }
 
       virtual unsigned int GetSubOperationCount() const
@@ -90,17 +99,78 @@
         const std::string& id = instances_[position_++];
 
         std::string dicom;
-        context_.ReadFile(dicom, id, FileContentType_Dicom);
+        context_.ReadDicom(dicom, id);
 
+        if (connection_.get() == NULL)
         {
-          ReusableDicomUserConnection::Locker locker
-            (context_.GetReusableDicomUserConnection(), localAet_, remote_);
-          locker.GetConnection().Store(dicom);
+          connection_.reset(new DicomUserConnection(localAet_, remote_));
         }
 
+        connection_->Store(dicom, originatorAet_, originatorId_);
+
         return Status_Success;
       }
     };
+
+
+    class AsynchronousMove : public IMoveRequestIterator
+    {
+    private:
+      ServerContext&                        context_;
+      std::auto_ptr<DicomModalityStoreJob>  job_;
+      size_t                                position_;
+      
+    public:
+      AsynchronousMove(ServerContext& context,
+                       const std::string& targetAet,
+                       const std::string& publicId,
+                       const std::string& originatorAet,
+                       uint16_t originatorId) :
+        context_(context),
+        job_(new DicomModalityStoreJob(context)),
+        position_(0)
+      {
+        LOG(INFO) << "Sending resource " << publicId << " to modality \"" << targetAet << "\"";
+
+        job_->SetDescription("C-MOVE");
+        job_->SetPermissive(true);
+        job_->SetLocalAet(context.GetDefaultLocalApplicationEntityTitle());
+        job_->SetRemoteModality(Configuration::GetModalityUsingAet(targetAet));
+
+        if (originatorId != 0)
+        {
+          job_->SetMoveOriginator(originatorAet, originatorId);
+        }
+        
+        std::list<std::string> tmp;
+        context_.GetIndex().GetChildInstances(tmp, publicId);
+
+        job_->Reserve(tmp.size());
+
+        for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it)
+        {
+          job_->AddInstance(*it);
+        }
+      }
+
+      virtual unsigned int GetSubOperationCount() const
+      {
+        return 1;
+      }
+
+      virtual Status DoNext()
+      {
+        if (position_ == 0)
+        {
+          context_.GetJobsEngine().GetRegistry().Submit(job_.release(), 0 /* priority */);
+          return Status_Success;
+        }
+        else
+        {
+          return Status_Failure;
+        }
+      }
+    };
   }
 
 
@@ -162,10 +232,31 @@
   }
 
 
+  static IMoveRequestIterator* CreateIterator(ServerContext& context,
+                                              const std::string& targetAet,
+                                              const std::string& publicId,
+                                              const std::string& originatorAet,
+                                              uint16_t originatorId)
+  {
+    bool synchronous = Configuration::GetGlobalBoolParameter("SynchronousCMove", false);
+
+    if (synchronous)
+    {
+      return new SynchronousMove(context, targetAet, publicId, originatorAet, originatorId);
+    }
+    else
+    {
+      return new AsynchronousMove(context, targetAet, publicId, originatorAet, originatorId);
+    }
+  }
+
+
   IMoveRequestIterator* OrthancMoveRequestHandler::Handle(const std::string& targetAet,
                                                           const DicomMap& input,
-                                                          const std::string& remoteIp,
-                                                          const std::string& remoteAet)
+                                                          const std::string& originatorIp,
+                                                          const std::string& originatorAet,
+                                                          const std::string& calledAet,
+                                                          uint16_t originatorId)
   {
     LOG(WARNING) << "Move-SCU request received for AET \"" << targetAet << "\"";
 
@@ -176,14 +267,12 @@
         if (!query.GetElement(i).GetValue().IsNull())
         {
           LOG(INFO) << "  " << query.GetElement(i).GetTag()
-                    << "  " << FromDcmtkBridge::GetName(query.GetElement(i).GetTag())
+                    << "  " << FromDcmtkBridge::GetTagName(query.GetElement(i))
                     << " = " << query.GetElement(i).GetValue().GetContent();
         }
       }
     }
 
-
-
     /**
      * Retrieve the query level.
      **/
@@ -207,7 +296,7 @@
           LookupIdentifier(publicId, ResourceType_Study, input) ||
           LookupIdentifier(publicId, ResourceType_Patient, input))
       {
-        return new OrthancMoveRequestIterator(context_, targetAet, publicId);
+        return CreateIterator(context_, targetAet, publicId, originatorAet, originatorId);
       }
       else
       {
@@ -228,7 +317,7 @@
 
     if (LookupIdentifier(publicId, level, input))
     {
-      return new OrthancMoveRequestIterator(context_, targetAet, publicId);
+      return CreateIterator(context_, targetAet, publicId, originatorAet, originatorId);
     }
     else
     {
--- a/OrthancServer/OrthancMoveRequestHandler.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancMoveRequestHandler.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -31,7 +32,7 @@
 
 #pragma once
 
-#include "DicomProtocol/IMoveRequestHandler.h"
+#include "../Core/DicomNetworking/IMoveRequestHandler.h"
 #include "ServerContext.h"
 
 namespace Orthanc
@@ -53,7 +54,9 @@
 
     virtual IMoveRequestIterator* Handle(const std::string& targetAet,
                                          const DicomMap& input,
-                                         const std::string& remoteIp,
-                                         const std::string& remoteAet);
+                                         const std::string& originatorIp,
+                                         const std::string& originatorAet,
+                                         const std::string& calledAet,
+                                         uint16_t originatorId);
   };
 }
--- a/OrthancServer/OrthancPeerParameters.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "PrecompiledHeadersServer.h"
-#include "OrthancPeerParameters.h"
-
-#include "../Core/OrthancException.h"
-
-namespace Orthanc
-{
-  OrthancPeerParameters::OrthancPeerParameters() : 
-    url_("http://localhost:8042/")
-  {
-  }
-
-
-  void OrthancPeerParameters::FromJson(const Json::Value& peer)
-  {
-    if (!peer.isArray() ||
-        (peer.size() != 1 && peer.size() != 3))
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    std::string url;
-
-    try
-    {
-      url = peer.get(0u, "").asString();
-
-      if (peer.size() == 1)
-      {
-        SetUsername("");
-        SetPassword("");
-      }
-      else if (peer.size() == 3)
-      {
-        SetUsername(peer.get(1u, "").asString());
-        SetPassword(peer.get(2u, "").asString());
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-    }
-    catch (...)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    if (url.size() != 0 && url[url.size() - 1] != '/')
-    {
-      url += '/';
-    }
-
-    SetUrl(url);
-  }
-
-
-  void OrthancPeerParameters::ToJson(Json::Value& value) const
-  {
-    value = Json::arrayValue;
-    value.append(GetUrl());
-    value.append(GetUsername());
-    value.append(GetPassword());
-  }
-}
--- a/OrthancServer/OrthancPeerParameters.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include <string>
-#include <json/json.h>
-
-namespace Orthanc
-{
-  class OrthancPeerParameters
-  {
-  private:
-    std::string url_;
-    std::string username_;
-    std::string password_;
-
-  public:
-    OrthancPeerParameters();
-
-    const std::string& GetUrl() const
-    {
-      return url_;
-    }
-
-    void SetUrl(const std::string& url)
-    {
-      url_ = url;
-    }
-
-    const std::string& GetUsername() const
-    {
-      return username_;
-    }
-
-    void SetUsername(const std::string& username)
-    {
-      username_ = username;
-    }
-    
-    const std::string& GetPassword() const
-    {
-      return password_;
-    }
-
-    void SetPassword(const std::string& password)
-    {
-      password_ = password;
-    }
-
-    void FromJson(const Json::Value& peer);
-
-    void ToJson(Json::Value& value) const;
-  };
-}
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,11 +34,13 @@
 #include "../PrecompiledHeadersServer.h"
 #include "OrthancRestApi.h"
 
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/Logging.h"
-#include "../../Core/Uuid.h"
-#include "../FromDcmtkBridge.h"
+#include "../../Core/SerializationToolbox.h"
 #include "../ServerContext.h"
-#include "../OrthancInitialization.h"
+#include "../ServerJobs/MergeStudyJob.h"
+#include "../ServerJobs/ResourceModificationJob.h"
+#include "../ServerJobs/SplitStudyJob.h"
 
 #include <boost/lexical_cast.hpp>
 #include <boost/algorithm/string/predicate.hpp>
@@ -46,69 +49,7 @@
 {
   // Modification of DICOM instances ------------------------------------------
 
-  enum TagOperation
-  {
-    TagOperation_Keep,
-    TagOperation_Remove
-  };
-
-  static void ParseListOfTags(DicomModification& target,
-                              const Json::Value& query,
-                              TagOperation operation)
-  {
-    if (!query.isArray())
-    {
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-
-    for (Json::Value::ArrayIndex i = 0; i < query.size(); i++)
-    {
-      std::string name = query[i].asString();
-
-      DicomTag tag = FromDcmtkBridge::ParseTag(name);
-
-      switch (operation)
-      {
-        case TagOperation_Keep:
-          target.Keep(tag);
-          VLOG(1) << "Keep: " << name << " " << tag << std::endl;
-          break;
-
-        case TagOperation_Remove:
-          target.Remove(tag);
-          VLOG(1) << "Remove: " << name << " " << tag << std::endl;
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-  }
-
-
-  static void ParseReplacements(DicomModification& target,
-                                const Json::Value& replacements)
-  {
-    if (!replacements.isObject())
-    {
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-
-    Json::Value::Members members = replacements.getMemberNames();
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      const std::string& name = members[i];
-      const Json::Value& value = replacements[name];
-
-      DicomTag tag = FromDcmtkBridge::ParseTag(name);
-      target.Replace(tag, value);
-
-      VLOG(1) << "Replace: " << name << " " << tag 
-              << " == " << value.toStyledString() << std::endl;
-    }
-  }
-
-
+  
   static std::string GeneratePatientName(ServerContext& context)
   {
     uint64_t seq = context.GetIndex().IncrementGlobalSequence(GlobalProperty_AnonymizationSequence);
@@ -116,97 +57,45 @@
   }
 
 
-
-  bool OrthancRestApi::ParseModifyRequest(DicomModification& target,
-                                          const Json::Value& request)
+  static void ParseModifyRequest(Json::Value& request,
+                                 DicomModification& target,
+                                 const RestApiPostCall& call)
   {
-    if (request.isObject())
-    {
-      if (request.isMember("RemovePrivateTags"))
-      {
-        target.SetRemovePrivateTags(true);
-      }
+    // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"},"Priority":9}'
 
-      if (request.isMember("Remove"))
-      {
-        ParseListOfTags(target, request["Remove"], TagOperation_Remove);
-      }
-
-      if (request.isMember("Replace"))
-      {
-        ParseReplacements(target, request["Replace"]);
-      }
-
-      return true;
+    if (call.ParseJsonRequest(request))
+    {
+      target.ParseModifyRequest(request);
     }
     else
     {
-      return false;
+      throw OrthancException(ErrorCode_BadFileFormat);
     }
   }
 
 
-  static bool ParseModifyRequest(DicomModification& target,
-                                 const RestApiPostCall& call)
-  {
-    // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"}}'
-
-    Json::Value request;
-    if (call.ParseJsonRequest(request))
-    {
-      return OrthancRestApi::ParseModifyRequest(target, request);
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  static bool ParseAnonymizationRequest(DicomModification& target,
+  static void ParseAnonymizationRequest(Json::Value& request,
+                                        DicomModification& target,
                                         RestApiPostCall& call)
   {
-    // curl http://localhost:8042/instances/6e67da51-d119d6ae-c5667437-87b9a8a5-0f07c49f/anonymize -X POST -d '{"Replace":{"PatientName":"hello","0010-0020":"world"},"Keep":["StudyDescription", "SeriesDescription"],"KeepPrivateTags": null,"Remove":["Modality"]}' > Anonymized.dcm
-
-    target.SetupAnonymization();
-    std::string patientName = target.GetReplacementAsString(DICOM_TAG_PATIENT_NAME);
-
-    Json::Value request;
-    if (call.ParseJsonRequest(request) && request.isObject())
-    {
-      if (request.isMember("KeepPrivateTags"))
-      {
-        target.SetRemovePrivateTags(false);
-      }
+    // curl http://localhost:8042/instances/6e67da51-d119d6ae-c5667437-87b9a8a5-0f07c49f/anonymize -X POST -d '{"Replace":{"PatientName":"hello","0010-0020":"world"},"Keep":["StudyDescription", "SeriesDescription"],"KeepPrivateTags": true,"Remove":["Modality"]}' > Anonymized.dcm
 
-      if (request.isMember("Remove"))
-      {
-        ParseListOfTags(target, request["Remove"], TagOperation_Remove);
-      }
+    if (call.ParseJsonRequest(request) &&
+        request.isObject())
+    {
+      bool patientNameReplaced;
+      target.ParseAnonymizationRequest(patientNameReplaced, request);
 
-      if (request.isMember("Replace"))
-      {
-        ParseReplacements(target, request["Replace"]);
-      }
-
-      if (request.isMember("Keep"))
-      {
-        ParseListOfTags(target, request["Keep"], TagOperation_Keep);
-      }
-
-      if (target.IsReplaced(DICOM_TAG_PATIENT_NAME) &&
-          target.GetReplacement(DICOM_TAG_PATIENT_NAME) == patientName)
+      if (patientNameReplaced)
       {
         // Overwrite the random Patient's Name by one that is more
         // user-friendly (provided none was specified by the user)
         target.Replace(DICOM_TAG_PATIENT_NAME, GeneratePatientName(OrthancRestApi::GetContext(call)), true);
       }
-
-      return true;
     }
     else
     {
-      return false;
+      throw OrthancException(ErrorCode_BadFileFormat);
     }
   }
 
@@ -216,180 +105,44 @@
   {
     std::string id = call.GetUriComponent("id", "");
 
-    ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id);
+    std::auto_ptr<ParsedDicomFile> modified;
 
-    std::auto_ptr<ParsedDicomFile> modified(locker.GetDicom().Clone());
+    {
+      ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id);
+      modified.reset(locker.GetDicom().Clone(true));
+    }
+    
     modification.Apply(*modified);
     modified->Answer(call.GetOutput());
   }
 
 
-  static void AnonymizeOrModifyResource(DicomModification& modification,
-                                        MetadataType metadataType,
-                                        ChangeType changeType,
-                                        ResourceType resourceType,
-                                        RestApiPostCall& call)
-  {
-    bool isFirst = true;
-    Json::Value result(Json::objectValue);
-
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    typedef std::list<std::string> Instances;
-    Instances instances;
-    std::string id = call.GetUriComponent("id", "");
-    context.GetIndex().GetChildInstances(instances, id);
-
-    if (instances.empty())
-    {
-      return;
-    }
-
-
-    /**
-     * Loop over all the instances of the resource.
-     **/
-
-    for (Instances::const_iterator it = instances.begin(); 
-         it != instances.end(); ++it)
-    {
-      LOG(INFO) << "Modifying instance " << *it;
-
-      std::auto_ptr<ServerContext::DicomCacheLocker> locker;
-
-      try
-      {
-        locker.reset(new ServerContext::DicomCacheLocker(OrthancRestApi::GetContext(call), *it));
-      }
-      catch (OrthancException&)
-      {
-        // This child instance has been removed in between
-        continue;
-      }
-
-
-      ParsedDicomFile& original = locker->GetDicom();
-      DicomInstanceHasher originalHasher = original.GetHasher();
-
-
-      /**
-       * Compute the resulting DICOM instance.
-       **/
-
-      std::auto_ptr<ParsedDicomFile> modified(original.Clone());
-      modification.Apply(*modified);
-
-      DicomInstanceToStore toStore;
-      toStore.SetRestOrigin(call);
-      toStore.SetParsedDicomFile(*modified);
-
-
-      /**
-       * Prepare the metadata information to associate with the
-       * resulting DICOM instance (AnonymizedFrom/ModifiedFrom).
-       **/
-
-      DicomInstanceHasher modifiedHasher = modified->GetHasher();
-
-      if (originalHasher.HashSeries() != modifiedHasher.HashSeries())
-      {
-        toStore.AddMetadata(ResourceType_Series, metadataType, originalHasher.HashSeries());
-      }
-
-      if (originalHasher.HashStudy() != modifiedHasher.HashStudy())
-      {
-        toStore.AddMetadata(ResourceType_Study, metadataType, originalHasher.HashStudy());
-      }
-
-      if (originalHasher.HashPatient() != modifiedHasher.HashPatient())
-      {
-        toStore.AddMetadata(ResourceType_Patient, metadataType, originalHasher.HashPatient());
-      }
-
-      assert(*it == originalHasher.HashInstance());
-      toStore.AddMetadata(ResourceType_Instance, metadataType, *it);
-
-
-      /**
-       * Store the resulting DICOM instance into the Orthanc store.
-       **/
-
-      std::string modifiedInstance;
-      if (context.Store(modifiedInstance, toStore) != StoreStatus_Success)
-      {
-        LOG(ERROR) << "Error while storing a modified instance " << *it;
-        throw OrthancException(ErrorCode_CannotStoreInstance);
-      }
-
-      // Sanity checks in debug mode
-      assert(modifiedInstance == modifiedHasher.HashInstance());
-
-
-      /**
-       * Compute the JSON object that is returned by the REST call.
-       **/
-
-      if (isFirst)
-      {
-        std::string newId;
-
-        switch (resourceType)
-        {
-          case ResourceType_Series:
-            newId = modifiedHasher.HashSeries();
-            break;
-
-          case ResourceType_Study:
-            newId = modifiedHasher.HashStudy();
-            break;
-
-          case ResourceType_Patient:
-            newId = modifiedHasher.HashPatient();
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_InternalError);
-        }
-
-        result["Type"] = EnumerationToString(resourceType);
-        result["ID"] = newId;
-        result["Path"] = GetBasePath(resourceType, newId);
-        result["PatientID"] = modifiedHasher.HashPatient();
-        isFirst = false;
-      }
-    }
-
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-
   static void ModifyInstance(RestApiPostCall& call)
   {
     DicomModification modification;
     modification.SetAllowManualIdentifiers(true);
 
-    if (ParseModifyRequest(modification, call))
+    Json::Value request;
+    ParseModifyRequest(request, modification, call);
+
+    if (modification.IsReplaced(DICOM_TAG_PATIENT_ID))
+    {
+      modification.SetLevel(ResourceType_Patient);
+    }
+    else if (modification.IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
     {
-      if (modification.IsReplaced(DICOM_TAG_PATIENT_ID))
-      {
-        modification.SetLevel(ResourceType_Patient);
-      }
-      else if (modification.IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
-      {
-        modification.SetLevel(ResourceType_Study);
-      }
-      else if (modification.IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
-      {
-        modification.SetLevel(ResourceType_Series);
-      }
-      else
-      {
-        modification.SetLevel(ResourceType_Instance);
-      }
+      modification.SetLevel(ResourceType_Study);
+    }
+    else if (modification.IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+    {
+      modification.SetLevel(ResourceType_Series);
+    }
+    else
+    {
+      modification.SetLevel(ResourceType_Instance);
+    }
 
-      AnonymizeOrModifyInstance(modification, call);
-    }
+    AnonymizeOrModifyInstance(modification, call);
   }
 
 
@@ -398,39 +151,58 @@
     DicomModification modification;
     modification.SetAllowManualIdentifiers(true);
 
-    if (ParseAnonymizationRequest(modification, call))
-    {
-      AnonymizeOrModifyInstance(modification, call);
-    }
+    Json::Value request;
+    ParseAnonymizationRequest(request, modification, call);
+
+    AnonymizeOrModifyInstance(modification, call);
   }
 
 
-  template <enum ChangeType changeType,
-            enum ResourceType resourceType>
-  static void ModifyResource(RestApiPostCall& call)
+  static void SubmitModificationJob(std::auto_ptr<DicomModification>& modification,
+                                    bool isAnonymization,
+                                    RestApiPostCall& call,
+                                    const Json::Value& body,
+                                    ResourceType level)
   {
-    DicomModification modification;
+    ServerContext& context = OrthancRestApi::GetContext(call);
 
-    if (ParseModifyRequest(modification, call))
-    {
-      modification.SetLevel(resourceType);
-      AnonymizeOrModifyResource(modification, MetadataType_ModifiedFrom, 
-                                changeType, resourceType, call);
-    }
+    std::auto_ptr<ResourceModificationJob> job(new ResourceModificationJob(context));
+    
+    job->SetModification(modification.release(), level, isAnonymization);
+    job->SetOrigin(call);
+    
+    context.AddChildInstances(*job, call.GetUriComponent("id", ""));
+
+    OrthancRestApi::GetApi(call).SubmitCommandsJob
+      (call, job.release(), true /* synchronous by default */, body);
   }
 
 
-  template <enum ChangeType changeType,
-            enum ResourceType resourceType>
+  template <enum ResourceType resourceType>
+  static void ModifyResource(RestApiPostCall& call)
+  {
+    std::auto_ptr<DicomModification> modification(new DicomModification);
+
+    Json::Value body;
+    ParseModifyRequest(body, *modification, call);
+
+    modification->SetLevel(resourceType);
+
+    SubmitModificationJob(modification, false /* not an anonymization */,
+                          call, body, resourceType);
+  }
+
+
+  template <enum ResourceType resourceType>
   static void AnonymizeResource(RestApiPostCall& call)
   {
-    DicomModification modification;
+    std::auto_ptr<DicomModification> modification(new DicomModification);
 
-    if (ParseAnonymizationRequest(modification, call))
-    {
-      AnonymizeOrModifyResource(modification, MetadataType_AnonymizedFrom, 
-                                changeType, resourceType, call);
-    }
+    Json::Value body;
+    ParseAnonymizationRequest(body, *modification, call);
+
+    SubmitModificationJob(modification, true /* anonymization */,
+                          call, body, resourceType);
   }
 
 
@@ -439,7 +211,7 @@
                                    ParsedDicomFile& dicom)
   {
     DicomInstanceToStore toStore;
-    toStore.SetRestOrigin(call);
+    toStore.SetOrigin(DicomInstanceOrigin::FromRest(call));
     toStore.SetParsedDicomFile(dicom);
 
     ServerContext& context = OrthancRestApi::GetContext(call);
@@ -480,7 +252,8 @@
       }
       else
       {
-        dicom.Replace(tag, value);
+        // This is V1, don't try and decode data URI scheme
+        dicom.ReplacePlainString(tag, value);
       }
     }
   }
@@ -526,7 +299,7 @@
         }
         else
         {
-          dicom.Replace(tag, tags[name], decodeBinaryTags);
+          dicom.Replace(tag, tags[name], decodeBinaryTags, DicomReplaceMode_InsertIfAbsent);
         }
       }
     }
@@ -542,8 +315,8 @@
     assert(content.size() > 0);
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    base.Replace(DICOM_TAG_IMAGES_IN_ACQUISITION, boost::lexical_cast<std::string>(content.size()));
-    base.Replace(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS, "1");
+    base.ReplacePlainString(DICOM_TAG_IMAGES_IN_ACQUISITION, boost::lexical_cast<std::string>(content.size()));
+    base.ReplacePlainString(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS, "1");
 
     std::string someInstance;
 
@@ -551,7 +324,7 @@
     {
       for (Json::ArrayIndex i = 0; i < content.size(); i++)
       {
-        std::auto_ptr<ParsedDicomFile> dicom(base.Clone());
+        std::auto_ptr<ParsedDicomFile> dicom(base.Clone(false));
         const Json::Value* payload = NULL;
 
         if (content[i].type() == Json::stringValue)
@@ -580,8 +353,8 @@
         }
 
         dicom->EmbedContent(payload->asString());
-        dicom->Replace(DICOM_TAG_INSTANCE_NUMBER, boost::lexical_cast<std::string>(i + 1));
-        dicom->Replace(DICOM_TAG_IMAGE_INDEX, boost::lexical_cast<std::string>(i + 1));
+        dicom->ReplacePlainString(DICOM_TAG_INSTANCE_NUMBER, boost::lexical_cast<std::string>(i + 1));
+        dicom->ReplacePlainString(DICOM_TAG_IMAGE_INDEX, boost::lexical_cast<std::string>(i + 1));
 
         StoreCreatedInstance(someInstance, call, *dicom);
       }
@@ -620,7 +393,7 @@
       throw OrthancException(ErrorCode_BadRequest);
     }
 
-    ParsedDicomFile dicom;
+    ParsedDicomFile dicom(true);
 
     {
       Encoding encoding;
@@ -636,8 +409,7 @@
       }
       else
       {
-        std::string tmp = Configuration::GetGlobalStringParameter("DefaultEncoding", "Latin1");
-        encoding = StringToEncoding(tmp.c_str());
+        encoding = GetDefaultDicomEncoding();
       }
 
       dicom.SetEncoding(encoding);
@@ -674,7 +446,7 @@
           throw OrthancException(ErrorCode_InternalError);
         }
 
-        context.ReadJson(siblingTags, siblingInstances.front());
+        context.ReadDicomAsJson(siblingTags, siblingInstances.front());
       }
 
 
@@ -731,12 +503,12 @@
           const Json::Value& tag = siblingTags[t];
           if (tag["Type"] == "Null")
           {
-            dicom.Replace(*it, "");
+            dicom.ReplacePlainString(*it, "");
           }
           else if (tag["Type"] == "String")
           {
             std::string value = tag["Value"].asString();
-            dicom.Replace(*it, Toolbox::ConvertFromUtf8(value, dicom.GetEncoding()));
+            dicom.ReplacePlainString(*it, Toolbox::ConvertFromUtf8(value, dicom.GetEncoding()));
           }
         }
       }
@@ -758,27 +530,27 @@
     
     // Inject time-related information
     std::string date, time;
-    Toolbox::GetNowDicom(date, time);
-    dicom.Replace(DICOM_TAG_ACQUISITION_DATE, date);
-    dicom.Replace(DICOM_TAG_ACQUISITION_TIME, time);
-    dicom.Replace(DICOM_TAG_CONTENT_DATE, date);
-    dicom.Replace(DICOM_TAG_CONTENT_TIME, time);
-    dicom.Replace(DICOM_TAG_INSTANCE_CREATION_DATE, date);
-    dicom.Replace(DICOM_TAG_INSTANCE_CREATION_TIME, time);
+    SystemToolbox::GetNowDicom(date, time, true /* use UTC time (not local time) */);
+    dicom.ReplacePlainString(DICOM_TAG_ACQUISITION_DATE, date);
+    dicom.ReplacePlainString(DICOM_TAG_ACQUISITION_TIME, time);
+    dicom.ReplacePlainString(DICOM_TAG_CONTENT_DATE, date);
+    dicom.ReplacePlainString(DICOM_TAG_CONTENT_TIME, time);
+    dicom.ReplacePlainString(DICOM_TAG_INSTANCE_CREATION_DATE, date);
+    dicom.ReplacePlainString(DICOM_TAG_INSTANCE_CREATION_TIME, time);
 
     if (parentType == ResourceType_Patient ||
         parentType == ResourceType_Study ||
         parentType == ResourceType_Instance /* no parent */)
     {
-      dicom.Replace(DICOM_TAG_SERIES_DATE, date);
-      dicom.Replace(DICOM_TAG_SERIES_TIME, time);
+      dicom.ReplacePlainString(DICOM_TAG_SERIES_DATE, date);
+      dicom.ReplacePlainString(DICOM_TAG_SERIES_TIME, time);
     }
 
     if (parentType == ResourceType_Patient ||
         parentType == ResourceType_Instance /* no parent */)
     {
-      dicom.Replace(DICOM_TAG_STUDY_DATE, date);
-      dicom.Replace(DICOM_TAG_STUDY_TIME, time);
+      dicom.ReplacePlainString(DICOM_TAG_STUDY_DATE, date);
+      dicom.ReplacePlainString(DICOM_TAG_STUDY_TIME, time);
     }
 
 
@@ -834,7 +606,7 @@
     else
     {
       // Compatibility with Orthanc <= 0.9.3
-      ParsedDicomFile dicom;
+      ParsedDicomFile dicom(true);
       CreateDicomV1(dicom, call, request);
 
       std::string id;
@@ -844,18 +616,141 @@
   }
 
 
+  static void SplitStudy(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value request;
+    if (!call.ParseJsonRequest(request))
+    {
+      // Bad JSON request
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    const std::string study = call.GetUriComponent("id", "");
+
+    std::auto_ptr<SplitStudyJob> job(new SplitStudyJob(context, study));    
+    job->SetOrigin(call);
+
+    std::vector<std::string> series;
+    SerializationToolbox::ReadArrayOfStrings(series, request, "Series");
+
+    for (size_t i = 0; i < series.size(); i++)
+    {
+      job->AddSourceSeries(series[i]);
+    }
+
+    job->AddTrailingStep();
+
+    static const char* KEEP_SOURCE = "KeepSource";
+    if (request.isMember(KEEP_SOURCE))
+    {
+      job->SetKeepSource(SerializationToolbox::ReadBoolean(request, KEEP_SOURCE));
+    }
+
+    static const char* REMOVE = "Remove";
+    if (request.isMember(REMOVE))
+    {
+      if (request[REMOVE].type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      for (Json::Value::ArrayIndex i = 0; i < request[REMOVE].size(); i++)
+      {
+        if (request[REMOVE][i].type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          job->Remove(FromDcmtkBridge::ParseTag(request[REMOVE][i].asCString()));
+        }
+      }
+    }
+
+    static const char* REPLACE = "Replace";
+    if (request.isMember(REPLACE))
+    {
+      if (request[REPLACE].type() != Json::objectValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      Json::Value::Members tags = request[REPLACE].getMemberNames();
+
+      for (size_t i = 0; i < tags.size(); i++)
+      {
+        const Json::Value& value = request[REPLACE][tags[i]];
+        
+        if (value.type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          job->Replace(FromDcmtkBridge::ParseTag(tags[i]), value.asString());
+        }
+      }
+    }
+
+    OrthancRestApi::GetApi(call).SubmitCommandsJob
+      (call, job.release(), true /* synchronous by default */, request);
+  }
+
+
+  static void MergeStudy(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value request;
+    if (!call.ParseJsonRequest(request))
+    {
+      // Bad JSON request
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    const std::string study = call.GetUriComponent("id", "");
+
+    std::auto_ptr<MergeStudyJob> job(new MergeStudyJob(context, study));    
+    job->SetOrigin(call);
+
+    std::vector<std::string> resources;
+    SerializationToolbox::ReadArrayOfStrings(resources, request, "Resources");
+
+    for (size_t i = 0; i < resources.size(); i++)
+    {
+      job->AddSource(resources[i]);
+    }
+
+    job->AddTrailingStep();
+
+    static const char* KEEP_SOURCE = "KeepSource";
+    if (request.isMember(KEEP_SOURCE))
+    {
+      job->SetKeepSource(SerializationToolbox::ReadBoolean(request, KEEP_SOURCE));
+    }
+
+    OrthancRestApi::GetApi(call).SubmitCommandsJob
+      (call, job.release(), true /* synchronous by default */, request);
+  }
+  
+
   void OrthancRestApi::RegisterAnonymizeModify()
   {
     Register("/instances/{id}/modify", ModifyInstance);
-    Register("/series/{id}/modify", ModifyResource<ChangeType_ModifiedSeries, ResourceType_Series>);
-    Register("/studies/{id}/modify", ModifyResource<ChangeType_ModifiedStudy, ResourceType_Study>);
-    Register("/patients/{id}/modify", ModifyResource<ChangeType_ModifiedPatient, ResourceType_Patient>);
+    Register("/series/{id}/modify", ModifyResource<ResourceType_Series>);
+    Register("/studies/{id}/modify", ModifyResource<ResourceType_Study>);
+    Register("/patients/{id}/modify", ModifyResource<ResourceType_Patient>);
 
     Register("/instances/{id}/anonymize", AnonymizeInstance);
-    Register("/series/{id}/anonymize", AnonymizeResource<ChangeType_ModifiedSeries, ResourceType_Series>);
-    Register("/studies/{id}/anonymize", AnonymizeResource<ChangeType_ModifiedStudy, ResourceType_Study>);
-    Register("/patients/{id}/anonymize", AnonymizeResource<ChangeType_ModifiedPatient, ResourceType_Patient>);
+    Register("/series/{id}/anonymize", AnonymizeResource<ResourceType_Series>);
+    Register("/studies/{id}/anonymize", AnonymizeResource<ResourceType_Study>);
+    Register("/patients/{id}/anonymize", AnonymizeResource<ResourceType_Patient>);
 
     Register("/tools/create-dicom", CreateDicom);
+
+    Register("/studies/{id}/split", SplitStudy);
+    Register("/studies/{id}/merge", MergeStudy);
   }
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,7 +35,7 @@
 #include "OrthancRestApi.h"
 
 #include "../../Core/Logging.h"
-#include "../DicomModification.h"
+#include "../../Core/SerializationToolbox.h"
 #include "../ServerContext.h"
 
 namespace Orthanc
@@ -93,7 +94,7 @@
     std::string postData(call.GetBodyData(), call.GetBodySize());
 
     DicomInstanceToStore toStore;
-    toStore.SetRestOrigin(call);
+    toStore.SetOrigin(DicomInstanceOrigin::FromRest(call));
     toStore.SetBuffer(postData);
 
     std::string publicId;
@@ -139,4 +140,102 @@
   {
     return GetContext(call).GetIndex();
   }
+
+
+
+  static const char* KEY_PERMISSIVE = "Permissive";
+  static const char* KEY_PRIORITY = "Priority";
+  static const char* KEY_SYNCHRONOUS = "Synchronous";
+  static const char* KEY_ASYNCHRONOUS = "Asynchronous";
+  
+  void OrthancRestApi::SubmitCommandsJob(RestApiPostCall& call,
+                                         SetOfCommandsJob* job,
+                                         bool isDefaultSynchronous,
+                                         const Json::Value& body) const
+  {
+    std::auto_ptr<SetOfCommandsJob> raii(job);
+    
+    if (job == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    if (body.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    job->SetDescription("REST API");
+    
+    if (body.isMember(KEY_PERMISSIVE))
+    {
+      job->SetPermissive(SerializationToolbox::ReadBoolean(body, KEY_PERMISSIVE));
+    }
+    else
+    {
+      job->SetPermissive(false);
+    }
+
+    int priority = 0;
+
+    if (body.isMember(KEY_PRIORITY))
+    {
+      priority = SerializationToolbox::ReadInteger(body, KEY_PRIORITY);
+    }
+
+    bool synchronous = isDefaultSynchronous;
+    
+    if (body.isMember(KEY_SYNCHRONOUS))
+    {
+      synchronous = SerializationToolbox::ReadBoolean(body, KEY_SYNCHRONOUS);
+    }
+    else if (body.isMember(KEY_ASYNCHRONOUS))
+    {
+      synchronous = !SerializationToolbox::ReadBoolean(body, KEY_ASYNCHRONOUS);
+    }
+
+    if (synchronous)
+    {
+      Json::Value successContent;
+      if (context_.GetJobsEngine().GetRegistry().SubmitAndWait
+          (successContent, raii.release(), priority))
+      {
+        // Success in synchronous execution
+        call.GetOutput().AnswerJson(successContent);
+      }
+      else
+      {
+        // Error during synchronous execution
+        call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
+      }
+    }
+    else
+    {
+      // Asynchronous mode: Submit the job, but don't wait for its completion
+      std::string id;
+      context_.GetJobsEngine().GetRegistry().Submit(id, raii.release(), priority);
+
+      Json::Value v;
+      v["ID"] = id;
+      v["Path"] = "/jobs/" + id;
+      call.GetOutput().AnswerJson(v);
+    }
+  }
+  
+
+  void OrthancRestApi::SubmitCommandsJob(RestApiPostCall& call,
+                                         SetOfCommandsJob* job,
+                                         bool isDefaultSynchronous) const
+  {
+    std::auto_ptr<SetOfCommandsJob> raii(job);
+    
+    Json::Value body;
+    
+    if (!call.ParseJsonRequest(body))
+    {
+      body = Json::objectValue;
+    }
+
+    SubmitCommandsJob(call, raii.release(), isDefaultSynchronous, body);
+  }
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestApi.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,8 +33,10 @@
 
 #pragma once
 
+#include "../../Core/JobsEngine/SetOfCommandsJob.h"
 #include "../../Core/RestApi/RestApi.h"
-#include "../DicomModification.h"
+#include "../../Core/DicomParsing/DicomModification.h"
+#include "../ServerEnumerations.h"
 
 #include <set>
 
@@ -95,7 +98,13 @@
                               ResourceType resourceType,
                               StoreStatus status) const;
 
-    static bool ParseModifyRequest(DicomModification& target,
-                                   const Json::Value& request);
+    void SubmitCommandsJob(RestApiPostCall& call,
+                           SetOfCommandsJob* job,
+                           bool isDefaultSynchronous,
+                           const Json::Value& body) const;
+
+    void SubmitCommandsJob(RestApiPostCall& call,
+                           SetOfCommandsJob* job,
+                           bool isDefaultSynchronous) const;
   };
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,349 +34,138 @@
 #include "../PrecompiledHeadersServer.h"
 #include "OrthancRestApi.h"
 
-#include "../DicomDirWriter.h"
-#include "../../Core/Compression/HierarchicalZipWriter.h"
 #include "../../Core/HttpServer/FilesystemHttpSender.h"
-#include "../../Core/Logging.h"
-#include "../../Core/Uuid.h"
-#include "../ServerContext.h"
-
-#include <stdio.h>
-
-#if defined(_MSC_VER)
-#define snprintf _snprintf
-#endif
-
-static const uint64_t MEGA_BYTES = 1024 * 1024;
-static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024;
+#include "../ServerJobs/ArchiveJob.h"
 
 namespace Orthanc
 {
-  // Download of ZIP files ----------------------------------------------------
- 
-  static std::string GetDirectoryNameInArchive(const Json::Value& resource,
-                                               ResourceType resourceType)
+  static bool AddResourcesOfInterest(ArchiveJob& job,
+                                     RestApiPostCall& call)
   {
-    std::string s;
-    const Json::Value& tags = resource["MainDicomTags"];
-
-    switch (resourceType)
+    Json::Value resources;
+    if (call.ParseJsonRequest(resources) &&
+        resources.type() == Json::arrayValue)
     {
-      case ResourceType_Patient:
+      for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
       {
-        std::string p = tags["PatientID"].asString();
-        std::string n = tags["PatientName"].asString();
-        s = p + " " + n;
-        break;
-      }
-
-      case ResourceType_Study:
-      {
-        std::string p;
-        if (tags.isMember("AccessionNumber"))
+        if (resources[i].type() != Json::stringValue)
         {
-          p = tags["AccessionNumber"].asString() + " ";
+          return false;   // Bad request
         }
 
-        s = p + tags["StudyDescription"].asString();
-        break;
-      }
-        
-      case ResourceType_Series:
-      {
-        std::string d = tags["SeriesDescription"].asString();
-        std::string m = tags["Modality"].asString();
-        s = m + " " + d;
-        break;
+        job.AddResource(resources[i].asString());
       }
-        
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
 
-    // Get rid of special characters
-    return Toolbox::ConvertToAscii(s);
-  }
-
-  static bool CreateRootDirectoryInArchive(HierarchicalZipWriter& writer,
-                                           ServerContext& context,
-                                           const Json::Value& resource,
-                                           ResourceType resourceType)
-  {
-    if (resourceType == ResourceType_Patient)
-    {
       return true;
     }
-
-    ResourceType parentType = GetParentResourceType(resourceType);
-    Json::Value parent;
-
-    switch (resourceType)
-    {
-      case ResourceType_Study:
-      {
-        if (!context.GetIndex().LookupResource(parent, resource["ParentPatient"].asString(), parentType))
-        {
-          return false;
-        }
-
-        break;
-      }
-        
-      case ResourceType_Series:
-        if (!context.GetIndex().LookupResource(parent, resource["ParentStudy"].asString(), parentType) ||
-            !CreateRootDirectoryInArchive(writer, context, parent, parentType))
-        {
-          return false;
-        }
-        break;
-        
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-
-    writer.OpenDirectory(GetDirectoryNameInArchive(parent, parentType).c_str());
-    return true;
-  }
-
-  static bool ArchiveInstance(HierarchicalZipWriter& writer,
-                              ServerContext& context,
-                              const std::string& instancePublicId,
-                              const char* filename)
-  {
-    writer.OpenFile(filename);
-
-    std::string dicom;
-    context.ReadFile(dicom, instancePublicId, FileContentType_Dicom);
-    writer.Write(dicom);
-
-    return true;
-  }
-
-  static bool ArchiveInternal(HierarchicalZipWriter& writer,
-                              ServerContext& context,
-                              const std::string& publicId,
-                              ResourceType resourceType,
-                              bool isFirstLevel)
-  { 
-    Json::Value resource;
-    if (!context.GetIndex().LookupResource(resource, publicId, resourceType))
-    {
-      return false;
-    }    
-
-    if (isFirstLevel && 
-        !CreateRootDirectoryInArchive(writer, context, resource, resourceType))
+    else
     {
       return false;
     }
-
-    writer.OpenDirectory(GetDirectoryNameInArchive(resource, resourceType).c_str());
-
-    switch (resourceType)
-    {
-      case ResourceType_Patient:
-        for (Json::Value::ArrayIndex i = 0; i < resource["Studies"].size(); i++)
-        {
-          std::string studyId = resource["Studies"][i].asString();
-          if (!ArchiveInternal(writer, context, studyId, ResourceType_Study, false))
-          {
-            return false;
-          }
-        }
-        break;
+  }
 
-      case ResourceType_Study:
-        for (Json::Value::ArrayIndex i = 0; i < resource["Series"].size(); i++)
-        {
-          std::string seriesId = resource["Series"][i].asString();
-          if (!ArchiveInternal(writer, context, seriesId, ResourceType_Series, false))
-          {
-            return false;
-          }
-        }
-        break;
-
-      case ResourceType_Series:
-      {
-        // Create a filename prefix, depending on the modality
-        char format[24] = "%08d.dcm";
 
-        if (resource["MainDicomTags"].isMember("Modality"))
-        {
-          std::string modality = resource["MainDicomTags"]["Modality"].asString();
-
-          if (modality.size() == 1)
-          {
-            snprintf(format, sizeof(format) - 1, "%c%%07d.dcm", toupper(modality[0]));
-          }
-          else if (modality.size() >= 2)
-          {
-            snprintf(format, sizeof(format) - 1, "%c%c%%06d.dcm", toupper(modality[0]), toupper(modality[1]));
-          }
-        }
-
-        char filename[24];
-
-        for (Json::Value::ArrayIndex i = 0; i < resource["Instances"].size(); i++)
-        {
-          snprintf(filename, sizeof(filename) - 1, format, i);
-
-          std::string publicId = resource["Instances"][i].asString();
-
-          // This was the implementation up to Orthanc 0.7.0:
-          // std::string filename = instance["MainDicomTags"]["SOPInstanceUID"].asString() + ".dcm";
-
-          if (!ArchiveInstance(writer, context, publicId, filename))
-          {
-            return false;
-          }
-        }
-
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
+  static void SubmitJob(RestApiCall& call,
+                        boost::shared_ptr<TemporaryFile>& tmp,
+                        ServerContext& context,
+                        std::auto_ptr<ArchiveJob>& job,
+                        const std::string& filename)
+  {
+    if (job.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
     }
 
-    writer.CloseDirectory();
-    return true;
-  }                                 
+    job->SetDescription("REST API");
 
+    Json::Value publicContent;
+    if (context.GetJobsEngine().GetRegistry().SubmitAndWait
+        (publicContent, job.release(), 0 /* TODO priority */))
+    {
+      // The archive is now created: Prepare the sending of the ZIP file
+      FilesystemHttpSender sender(tmp->GetPath());
+      sender.SetContentType("application/zip");
+      sender.SetContentFilename(filename);
 
-  static bool IsZip64Required(ServerIndex& index,
-                              const std::string& id)
+      // Send the ZIP
+      call.GetOutput().AnswerStream(sender);
+    }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
+    }      
+  }
+
+  
+  static void CreateBatchArchive(RestApiPostCall& call)
   {
-    /**
-     * Determine whether ZIP64 is required. Original ZIP format can
-     * store up to 2GB of data (some implementation supporting up to
-     * 4GB of data), and up to 65535 files.
-     * https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64
-     **/
+    ServerContext& context = OrthancRestApi::GetContext(call);
 
-    uint64_t uncompressedSize;
-    uint64_t compressedSize;
-    unsigned int countStudies;
-    unsigned int countSeries;
-    unsigned int countInstances;
-    index.GetStatistics(compressedSize, uncompressedSize, 
-                        countStudies, countSeries, countInstances, id);
-    const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES ||
-                          countInstances >= 65535);
+    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
+    std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, false, false));
+
+    if (AddResourcesOfInterest(*job, call))
+    {
+      SubmitJob(call, tmp, context, job, "Archive.zip");
+    }
+  }  
 
-    LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size "
-              << (uncompressedSize / MEGA_BYTES) << "MB using the "
-              << (isZip64 ? "ZIP64" : "ZIP32") << " file format";
+  
+  template <bool Extended>
+  static void CreateBatchMedia(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
+    std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, true, Extended));
 
-    return isZip64;
+    if (AddResourcesOfInterest(*job, call))
+    {
+      SubmitJob(call, tmp, context, job, "Archive.zip");
+    }
   }
-                              
+  
 
-  template <enum ResourceType resourceType>
-  static void GetArchive(RestApiGetCall& call)
+  static void CreateArchive(RestApiGetCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::string id = call.GetUriComponent("id", "");
-    bool isZip64 = IsZip64Required(context.GetIndex(), id);
-
-    // Create a RAII for the temporary file to manage the ZIP file
-    Toolbox::TemporaryFile tmp;
-
-    {
-      // Create a ZIP writer
-      HierarchicalZipWriter writer(tmp.GetPath().c_str());
-      writer.SetZip64(isZip64);
 
-      // Store the requested resource into the ZIP
-      if (!ArchiveInternal(writer, context, id, resourceType, true))
-      {
-        return;
-      }
-    }
+    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
+    std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, false, false));
+    job->AddResource(id);
 
-    // Prepare the sending of the ZIP file
-    FilesystemHttpSender sender(tmp.GetPath());
-    sender.SetContentType("application/zip");
-    sender.SetContentFilename(id + ".zip");
-
-    // Send the ZIP
-    call.GetOutput().AnswerStream(sender);
-
-    // The temporary file is automatically removed thanks to the RAII
+    SubmitJob(call, tmp, context, job, id + ".zip");
   }
 
 
-  static void GetMediaArchive(RestApiGetCall& call)
+  static void CreateMedia(RestApiGetCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::string id = call.GetUriComponent("id", "");
-    bool isZip64 = IsZip64Required(context.GetIndex(), id);
-
-    // Create a RAII for the temporary file to manage the ZIP file
-    Toolbox::TemporaryFile tmp;
-
-    {
-      // Create a ZIP writer
-      HierarchicalZipWriter writer(tmp.GetPath().c_str());
-      writer.SetZip64(isZip64);
-      writer.OpenDirectory("IMAGES");
-
-      // Create the DICOMDIR writer
-      DicomDirWriter dicomDir;
-
-      // Retrieve the list of the instances
-      std::list<std::string> instances;
-      context.GetIndex().GetChildInstances(instances, id);
 
-      size_t pos = 0;
-      for (std::list<std::string>::const_iterator
-             it = instances.begin(); it != instances.end(); ++it, ++pos)
-      {
-        // "DICOM restricts the filenames on DICOM media to 8
-        // characters (some systems wrongly use 8.3, but this does not
-        // conform to the standard)."
-        std::string filename = "IM" + boost::lexical_cast<std::string>(pos);
-        writer.OpenFile(filename.c_str());
-
-        std::string dicom;
-        context.ReadFile(dicom, *it, FileContentType_Dicom);
-        writer.Write(dicom);
-
-        ParsedDicomFile parsed(dicom);
-        dicomDir.Add("IMAGES", filename, parsed);
-      }
+    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
+    std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, true, call.HasArgument("extended")));
+    job->AddResource(id);
 
-      // Add the DICOMDIR
-      writer.CloseDirectory();
-      writer.OpenFile("DICOMDIR");
-      std::string s;
-      dicomDir.Encode(s);
-      writer.Write(s);
-    }
-
-    // Prepare the sending of the ZIP file
-    FilesystemHttpSender sender(tmp.GetPath());
-    sender.SetContentType("application/zip");
-    sender.SetContentFilename(id + ".zip");
-
-    // Send the ZIP
-    call.GetOutput().AnswerStream(sender);
-
-    // The temporary file is automatically removed thanks to the RAII
+    SubmitJob(call, tmp, context, job, id + ".zip");
   }
 
 
   void OrthancRestApi::RegisterArchive()
   {
-    Register("/patients/{id}/archive", GetArchive<ResourceType_Patient>);
-    Register("/studies/{id}/archive", GetArchive<ResourceType_Study>);
-    Register("/series/{id}/archive", GetArchive<ResourceType_Series>);
+    Register("/patients/{id}/archive", CreateArchive);
+    Register("/studies/{id}/archive", CreateArchive);
+    Register("/series/{id}/archive", CreateArchive);
 
-    Register("/patients/{id}/media", GetMediaArchive);
-    Register("/studies/{id}/media", GetMediaArchive);
-    Register("/series/{id}/media", GetMediaArchive);
+    Register("/patients/{id}/media", CreateMedia);
+    Register("/studies/{id}/media", CreateMedia);
+    Register("/series/{id}/media", CreateMedia);
+
+    Register("/tools/create-archive", CreateBatchArchive);
+    Register("/tools/create-media", CreateBatchMedia<false>);
+    Register("/tools/create-media-extended", CreateBatchMedia<true>);
   }
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestChanges.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestChanges.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -59,7 +60,7 @@
       since = boost::lexical_cast<int64_t>(call.GetArgument("since", "0"));
       limit = boost::lexical_cast<unsigned int>(call.GetArgument("limit", "0"));
     }
-    catch (boost::bad_lexical_cast)
+    catch (boost::bad_lexical_cast&)
     {
       return;
     }
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,16 +34,17 @@
 #include "../PrecompiledHeadersServer.h"
 #include "OrthancRestApi.h"
 
-#include "../OrthancInitialization.h"
-#include "../../Core/HttpClient.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/Logging.h"
-#include "../FromDcmtkBridge.h"
-#include "../Scheduler/ServerJob.h"
-#include "../Scheduler/StoreScuCommand.h"
-#include "../Scheduler/StorePeerCommand.h"
+#include "../../Core/SerializationToolbox.h"
+#include "../OrthancInitialization.h"
 #include "../QueryRetrieveHandler.h"
+#include "../ServerJobs/DicomModalityStoreJob.h"
+#include "../ServerJobs/DicomMoveScuJob.h"
+#include "../ServerJobs/OrthancPeerStoreJob.h"
 #include "../ServerToolbox.h"
 
+
 namespace Orthanc
 {
   /***************************************************************************
@@ -54,12 +56,15 @@
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-    RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote);
+    RemoteModalityParameters remote =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
     try
     {
-      if (locker.GetConnection().Echo())
+      DicomUserConnection connection(localAet, remote);
+      connection.Open();
+      
+      if (connection.Echo())
       {
         // Echo has succeeded
         call.GetOutput().AnswerBuffer("{}", "application/json");
@@ -97,7 +102,7 @@
     for (size_t i = 0; i < members.size(); i++)
     {
       DicomTag t = FromDcmtkBridge::ParseTag(members[i]);
-      result.SetValue(t, query[members[i]].asString());
+      result.SetValue(t, query[members[i]].asString(), false);
     }
 
     return true;
@@ -175,11 +180,16 @@
     }
 
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-    RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote);
+    RemoteModalityParameters remote =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+    
+    DicomFindAnswers answers(false);
 
-    DicomFindAnswers answers;
-    FindPatient(answers, locker.GetConnection(), fields);
+    {
+      DicomUserConnection connection(localAet, remote);
+      connection.Open();
+      FindPatient(answers, connection, fields);
+    }
 
     Json::Value result;
     answers.ToJson(result, true);
@@ -205,11 +215,16 @@
     }        
       
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-    RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote);
+    RemoteModalityParameters remote =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+
+    DicomFindAnswers answers(false);
 
-    DicomFindAnswers answers;
-    FindStudy(answers, locker.GetConnection(), fields);
+    {
+      DicomUserConnection connection(localAet, remote);
+      connection.Open();
+      FindStudy(answers, connection, fields);
+    }
 
     Json::Value result;
     answers.ToJson(result, true);
@@ -236,11 +251,16 @@
     }        
          
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-    RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote);
+    RemoteModalityParameters remote =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+
+    DicomFindAnswers answers(false);
 
-    DicomFindAnswers answers;
-    FindSeries(answers, locker.GetConnection(), fields);
+    {
+      DicomUserConnection connection(localAet, remote);
+      connection.Open();
+      FindSeries(answers, connection, fields);
+    }
 
     Json::Value result;
     answers.ToJson(result, true);
@@ -268,11 +288,16 @@
     }        
          
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-    RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote);
+    RemoteModalityParameters remote =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+
+    DicomFindAnswers answers(false);
 
-    DicomFindAnswers answers;
-    FindInstance(answers, locker.GetConnection(), fields);
+    {
+      DicomUserConnection connection(localAet, remote);
+      connection.Open();
+      FindInstance(answers, connection, fields);
+    }
 
     Json::Value result;
     answers.ToJson(result, true);
@@ -280,6 +305,18 @@
   }
 
 
+  static void CopyTagIfExists(DicomMap& target,
+                              ParsedDicomFile& source,
+                              const DicomTag& tag)
+  {
+    std::string tmp;
+    if (source.GetTagValue(tmp, tag))
+    {
+      target.SetValue(tag, tmp, false);
+    }
+  }
+
+
   static void DicomFind(RestApiPostCall& call)
   {
     LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
@@ -293,54 +330,59 @@
     }
  
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-    RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote);
+    RemoteModalityParameters remote =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
-    DicomFindAnswers patients;
-    FindPatient(patients, locker.GetConnection(), m);
+    DicomUserConnection connection(localAet, remote);
+    connection.Open();
+    
+    DicomFindAnswers patients(false);
+    FindPatient(patients, connection, m);
 
     // Loop over the found patients
     Json::Value result = Json::arrayValue;
     for (size_t i = 0; i < patients.GetSize(); i++)
     {
-      Json::Value patient(Json::objectValue);
-      FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i), true);
+      Json::Value patient;
+      patients.ToJson(patient, i, true);
 
       DicomMap::SetupFindStudyTemplate(m);
       if (!MergeQueryAndTemplate(m, call.GetBodyData(), call.GetBodySize()))
       {
         return;
       }
-      m.CopyTagIfExists(patients.GetAnswer(i), DICOM_TAG_PATIENT_ID);
+
+      CopyTagIfExists(m, patients.GetAnswer(i), DICOM_TAG_PATIENT_ID);
 
-      DicomFindAnswers studies;
-      FindStudy(studies, locker.GetConnection(), m);
+      DicomFindAnswers studies(false);
+      FindStudy(studies, connection, m);
 
       patient["Studies"] = Json::arrayValue;
       
       // Loop over the found studies
       for (size_t j = 0; j < studies.GetSize(); j++)
       {
-        Json::Value study(Json::objectValue);
-        FromDcmtkBridge::ToJson(study, studies.GetAnswer(j), true);
+        Json::Value study;
+        studies.ToJson(study, j, true);
 
         DicomMap::SetupFindSeriesTemplate(m);
         if (!MergeQueryAndTemplate(m, call.GetBodyData(), call.GetBodySize()))
         {
           return;
         }
-        m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_PATIENT_ID);
-        m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID);
 
-        DicomFindAnswers series;
-        FindSeries(series, locker.GetConnection(), m);
+        CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_PATIENT_ID);
+        CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID);
+
+        DicomFindAnswers series(false);
+        FindSeries(series, connection, m);
 
         // Loop over the found series
         study["Series"] = Json::arrayValue;
         for (size_t k = 0; k < series.GetSize(); k++)
         {
-          Json::Value series2(Json::objectValue);
-          FromDcmtkBridge::ToJson(series2, series.GetAnswer(k), true);
+          Json::Value series2;
+          series.ToJson(series2, k, true);
           study["Series"].append(series2);
         }
 
@@ -372,7 +414,7 @@
       std::auto_ptr<QueryRetrieveHandler>  handler(new QueryRetrieveHandler(context));
 
       handler->SetModality(call.GetUriComponent("id", ""));
-      handler->SetLevel(StringToResourceType(request["Level"].asString().c_str()));
+      handler->SetLevel(StringToResourceType(request["Level"].asCString()));
 
       if (request.isMember("Query"))
       {
@@ -390,7 +432,7 @@
       Json::Value result = Json::objectValue;
       result["ID"] = s;
       result["Path"] = "/queries/" + s;
-      call.GetOutput().AnswerJson(result);      
+      call.GetOutput().AnswerJson(result);
     }
   }
 
@@ -430,9 +472,9 @@
       {
       }                     
 
-      QueryRetrieveHandler* operator->()
+      QueryRetrieveHandler& GetHandler() const
       {
-        return &handler_;
+        return handler_;
       }
     };
 
@@ -450,7 +492,7 @@
   static void ListQueryAnswers(RestApiGetCall& call)
   {
     QueryAccessor query(call);
-    size_t count = query->GetAnswerCount();
+    size_t count = query.GetHandler().GetAnswersCount();
 
     Json::Value result = Json::arrayValue;
     for (size_t i = 0; i < count; i++)
@@ -465,61 +507,96 @@
   static void GetQueryOneAnswer(RestApiGetCall& call)
   {
     size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
+
     QueryAccessor query(call);
-    AnswerDicomMap(call, query->GetAnswer(index), call.HasArgument("simplify"));
+
+    DicomMap map;
+    query.GetHandler().GetAnswer(map, index);
+
+    AnswerDicomMap(call, map, call.HasArgument("simplify"));
   }
 
 
+  static void SubmitRetrieveJob(RestApiPostCall& call,
+                                bool allAnswers,
+                                size_t index)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::string targetAet;
+    
+    Json::Value body;
+    if (call.ParseJsonRequest(body))
+    {
+      targetAet = SerializationToolbox::ReadString(body, "TargetAet");
+    }
+    else
+    {
+      body = Json::objectValue;
+      call.BodyToString(targetAet);
+    }
+    
+    std::auto_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
+    
+    {
+      QueryAccessor query(call);
+      job->SetTargetAet(targetAet);
+      job->SetLocalAet(query.GetHandler().GetLocalAet());
+      job->SetRemoteModality(query.GetHandler().GetRemoteModality());
+
+      LOG(WARNING) << "Driving C-Move SCU on remote modality "
+                   << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle()
+                   << " to target modality " << targetAet;
+
+      if (allAnswers)
+      {
+        for (size_t i = 0; i < query.GetHandler().GetAnswersCount(); i++)
+        {
+          job->AddFindAnswer(query.GetHandler(), i);
+        }
+      }
+      else
+      {
+        job->AddFindAnswer(query.GetHandler(), index);
+      }
+    }
+
+    OrthancRestApi::GetApi(call).SubmitCommandsJob
+      (call, job.release(), true /* synchronous by default */, body);
+  }
+  
+
   static void RetrieveOneAnswer(RestApiPostCall& call)
   {
     size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
-
-    std::string modality;
-    call.BodyToString(modality);
-
-    LOG(WARNING) << "Driving C-Move SCU on modality: " << modality;
-
-    QueryAccessor query(call);
-    query->Retrieve(modality, index);
-
-    // Retrieve has succeeded
-    call.GetOutput().AnswerBuffer("{}", "application/json");
+    SubmitRetrieveJob(call, false, index);
   }
 
 
   static void RetrieveAllAnswers(RestApiPostCall& call)
   {
-    std::string modality;
-    call.BodyToString(modality);
-
-    LOG(WARNING) << "Driving C-Move SCU on modality: " << modality;
-
-    QueryAccessor query(call);
-    query->Retrieve(modality);
-
-    // Retrieve has succeeded
-    call.GetOutput().AnswerBuffer("{}", "application/json");
+    SubmitRetrieveJob(call, true, 0);
   }
 
 
   static void GetQueryArguments(RestApiGetCall& call)
   {
     QueryAccessor query(call);
-    AnswerDicomMap(call, query->GetQuery(), call.HasArgument("simplify"));
+    AnswerDicomMap(call, query.GetHandler().GetQuery(), call.HasArgument("simplify"));
   }
 
 
   static void GetQueryLevel(RestApiGetCall& call)
   {
     QueryAccessor query(call);
-    call.GetOutput().AnswerBuffer(EnumerationToString(query->GetLevel()), "text/plain");
+    call.GetOutput().AnswerBuffer(EnumerationToString(query.GetHandler().GetLevel()), "text/plain");
   }
 
 
   static void GetQueryModality(RestApiGetCall& call)
   {
     QueryAccessor query(call);
-    call.GetOutput().AnswerBuffer(query->GetModalitySymbolicName(), "text/plain");
+    call.GetOutput().AnswerBuffer(query.GetHandler().GetModalitySymbolicName(), "text/plain");
   }
 
 
@@ -547,7 +624,9 @@
 
     // Ensure that the answer of interest does exist
     size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
-    query->GetAnswer(index);
+
+    DicomMap map;
+    query.GetHandler().GetAnswer(map, index);
 
     RestApi::AutoListChildren(call);
   }
@@ -560,7 +639,7 @@
    ***************************************************************************/
 
   static bool GetInstancesToExport(Json::Value& otherArguments,
-                                   std::list<std::string>& instances,
+                                   SetOfInstancesJob& job,
                                    const std::string& remote,
                                    RestApiPostCall& call)
   {
@@ -630,19 +709,12 @@
         return false;
       }
 
-      if (Configuration::GetGlobalBoolParameter("LogExportedResources", true))
+      if (Configuration::GetGlobalBoolParameter("LogExportedResources", false))
       {
         context.GetIndex().LogExportedResource(stripped, remote);
       }
-       
-      std::list<std::string> tmp;
-      context.GetIndex().GetChildInstances(tmp, stripped);
 
-      for (std::list<std::string>::const_iterator
-             it = tmp.begin(); it != tmp.end(); ++it)
-      {
-        instances.push_back(*it);
-      }
+      context.AddChildInstances(job, stripped);
     }
 
     return true;
@@ -656,39 +728,79 @@
     std::string remote = call.GetUriComponent("id", "");
 
     Json::Value request;
-    std::list<std::string> instances;
-    if (!GetInstancesToExport(request, instances, remote, call))
+    std::auto_ptr<DicomModalityStoreJob> job(new DicomModalityStoreJob(context));
+
+    if (GetInstancesToExport(request, *job, remote, call))
     {
-      return;
+      std::string localAet = Toolbox::GetJsonStringField
+        (request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle());
+      std::string moveOriginatorAET = Toolbox::GetJsonStringField
+        (request, "MoveOriginatorAet", context.GetDefaultLocalApplicationEntityTitle());
+      int moveOriginatorID = Toolbox::GetJsonIntegerField
+        (request, "MoveOriginatorID", 0 /* By default, not a C-MOVE */);
+
+      job->SetLocalAet(localAet);
+      job->SetRemoteModality(Configuration::GetModalityUsingSymbolicName(remote));
+
+      if (moveOriginatorID != 0)
+      {
+        job->SetMoveOriginator(moveOriginatorAET, moveOriginatorID);
+      }
+
+      OrthancRestApi::GetApi(call).SubmitCommandsJob
+        (call, job.release(), true /* synchronous by default */, request);
     }
+  }
 
-    std::string localAet = context.GetDefaultLocalApplicationEntityTitle();
-    if (request.isMember("LocalAet"))
+
+  /***************************************************************************
+   * DICOM C-Move SCU
+   ***************************************************************************/
+  
+  static void DicomMove(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value request;
+
+    static const char* RESOURCES = "Resources";
+    static const char* LEVEL = "Level";
+
+    if (!call.ParseJsonRequest(request) ||
+        request.type() != Json::objectValue ||
+        !request.isMember(RESOURCES) ||
+        !request.isMember(LEVEL) ||
+        request[RESOURCES].type() != Json::arrayValue ||
+        request[LEVEL].type() != Json::stringValue)
     {
-      localAet = request["LocalAet"].asString();
+      throw OrthancException(ErrorCode_BadFileFormat);
     }
 
-    RemoteModalityParameters p = Configuration::GetModalityUsingSymbolicName(remote);
+    ResourceType level = StringToResourceType(request["Level"].asCString());
+    
+    std::string localAet = Toolbox::GetJsonStringField
+      (request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle());
+    std::string targetAet = Toolbox::GetJsonStringField
+      (request, "TargetAet", context.GetDefaultLocalApplicationEntityTitle());
+
+    const RemoteModalityParameters source =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
-    ServerJob job;
-    for (std::list<std::string>::const_iterator 
-           it = instances.begin(); it != instances.end(); ++it)
+    DicomUserConnection connection(localAet, source);
+    connection.Open();
+    
+    for (Json::Value::ArrayIndex i = 0; i < request[RESOURCES].size(); i++)
     {
-      job.AddCommand(new StoreScuCommand(context, localAet, p, false)).AddInput(*it);
+      DicomMap resource;
+      FromDcmtkBridge::FromJson(resource, request[RESOURCES][i]);
+      
+      connection.Move(targetAet, level, resource);
     }
 
-    job.SetDescription("HTTP request: Store-SCU to peer \"" + remote + "\"");
+    // Move has succeeded
+    call.GetOutput().AnswerBuffer("{}", "application/json");
+  }
 
-    if (context.GetScheduler().SubmitAndWait(job))
-    {
-      // Success
-      call.GetOutput().AnswerBuffer("{}", "application/json");
-    }
-    else
-    {
-      call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
-    }
-  }
 
 
   /***************************************************************************
@@ -706,14 +818,41 @@
     OrthancRestApi::SetOfStrings peers;
     Configuration::GetListOfOrthancPeers(peers);
 
-    Json::Value result = Json::arrayValue;
-    for (OrthancRestApi::SetOfStrings::const_iterator 
-           it = peers.begin(); it != peers.end(); ++it)
+    if (call.HasArgument("expand"))
     {
-      result.append(*it);
+      Json::Value result = Json::objectValue;
+      for (OrthancRestApi::SetOfStrings::const_iterator
+             it = peers.begin(); it != peers.end(); ++it)
+      {
+        WebServiceParameters peer;
+        
+        if (Configuration::GetOrthancPeer(peer, *it))
+        {
+          Json::Value jsonPeer = Json::objectValue;
+          // only return the minimum information to identify the
+          // destination, do not include "security" information like
+          // passwords
+          jsonPeer["Url"] = peer.GetUrl();
+          if (!peer.GetUsername().empty())
+          {
+            jsonPeer["Username"] = peer.GetUsername();
+          }
+          result[*it] = jsonPeer;
+        }
+      }
+      call.GetOutput().AnswerJson(result);
     }
+    else // if expand is not present, keep backward compatibility and return an array of peers
+    {
+      Json::Value result = Json::arrayValue;
+      for (OrthancRestApi::SetOfStrings::const_iterator
+             it = peers.begin(); it != peers.end(); ++it)
+      {
+        result.append(*it);
+      }
 
-    call.GetOutput().AnswerJson(result);
+      call.GetOutput().AnswerJson(result);
+    }
   }
 
   static void ListPeerOperations(RestApiGetCall& call)
@@ -735,32 +874,22 @@
     std::string remote = call.GetUriComponent("id", "");
 
     Json::Value request;
-    std::list<std::string> instances;
-    if (!GetInstancesToExport(request, instances, remote, call))
-    {
-      return;
-    }
+    std::auto_ptr<OrthancPeerStoreJob> job(new OrthancPeerStoreJob(context));
 
-    OrthancPeerParameters peer;
-    Configuration::GetOrthancPeer(peer, remote);
-
-    ServerJob job;
-    for (std::list<std::string>::const_iterator 
-           it = instances.begin(); it != instances.end(); ++it)
+    if (GetInstancesToExport(request, *job, remote, call))
     {
-      job.AddCommand(new StorePeerCommand(context, peer, false)).AddInput(*it);
-    }
-
-    job.SetDescription("HTTP request: POST to peer \"" + remote + "\"");
-
-    if (context.GetScheduler().SubmitAndWait(job))
-    {
-      // Success
-      call.GetOutput().AnswerBuffer("{}", "application/json");
-    }
-    else
-    {
-      call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
+      WebServiceParameters peer;
+      if (Configuration::GetOrthancPeer(peer, remote))
+      {
+        job->SetPeer(peer);    
+        OrthancRestApi::GetApi(call).SubmitCommandsJob
+          (call, job.release(), true /* synchronous by default */, request);
+      }
+      else
+      {
+        LOG(ERROR) << "No peer with symbolic name: " << remote;
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
     }
   }
 
@@ -778,14 +907,30 @@
     OrthancRestApi::SetOfStrings modalities;
     Configuration::GetListOfDicomModalities(modalities);
 
-    Json::Value result = Json::arrayValue;
-    for (OrthancRestApi::SetOfStrings::const_iterator 
-           it = modalities.begin(); it != modalities.end(); ++it)
+    if (call.HasArgument("expand"))
     {
-      result.append(*it);
+      Json::Value result = Json::objectValue;
+      for (OrthancRestApi::SetOfStrings::const_iterator
+             it = modalities.begin(); it != modalities.end(); ++it)
+      {
+        const RemoteModalityParameters& remote = Configuration::GetModalityUsingSymbolicName(*it);
+        
+        Json::Value info;
+        remote.Serialize(info, true /* force advanced format */);
+        result[*it] = info;
+      }
+      call.GetOutput().AnswerJson(result);
     }
-
-    call.GetOutput().AnswerJson(result);
+    else // if expand is not present, keep backward compatibility and return an array of modalities ids
+    {
+      Json::Value result = Json::arrayValue;
+      for (OrthancRestApi::SetOfStrings::const_iterator
+             it = modalities.begin(); it != modalities.end(); ++it)
+      {
+        result.append(*it);
+      }
+      call.GetOutput().AnswerJson(result);
+    }
   }
 
 
@@ -804,13 +949,15 @@
 
   static void UpdateModality(RestApiPutCall& call)
   {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
     Json::Value json;
     Json::Reader reader;
     if (reader.parse(call.GetBodyData(), call.GetBodyData() + call.GetBodySize(), json))
     {
       RemoteModalityParameters modality;
-      modality.FromJson(json);
-      Configuration::UpdateModality(call.GetUriComponent("id", ""), modality);
+      modality.Unserialize(json);
+      Configuration::UpdateModality(context, call.GetUriComponent("id", ""), modality);
       call.GetOutput().AnswerBuffer("", "text/plain");
     }
   }
@@ -818,20 +965,24 @@
 
   static void DeleteModality(RestApiDeleteCall& call)
   {
-    Configuration::RemoveModality(call.GetUriComponent("id", ""));
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Configuration::RemoveModality(context, call.GetUriComponent("id", ""));
     call.GetOutput().AnswerBuffer("", "text/plain");
   }
 
 
   static void UpdatePeer(RestApiPutCall& call)
   {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
     Json::Value json;
     Json::Reader reader;
     if (reader.parse(call.GetBodyData(), call.GetBodyData() + call.GetBodySize(), json))
     {
-      OrthancPeerParameters peer;
-      peer.FromJson(json);
-      Configuration::UpdatePeer(call.GetUriComponent("id", ""), peer);
+      WebServiceParameters peer;
+      peer.Unserialize(json);
+      Configuration::UpdatePeer(context, call.GetUriComponent("id", ""), peer);
       call.GetOutput().AnswerBuffer("", "text/plain");
     }
   }
@@ -839,11 +990,41 @@
 
   static void DeletePeer(RestApiDeleteCall& call)
   {
-    Configuration::RemovePeer(call.GetUriComponent("id", ""));
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Configuration::RemovePeer(context, call.GetUriComponent("id", ""));
     call.GetOutput().AnswerBuffer("", "text/plain");
   }
 
 
+  static void DicomFindWorklist(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value json;
+    if (call.ParseJsonRequest(json))
+    {
+      const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
+      RemoteModalityParameters remote =
+        Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+
+      std::auto_ptr<ParsedDicomFile> query(ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0)));
+
+      DicomFindAnswers answers(true);
+
+      {
+        DicomUserConnection connection(localAet, remote);
+        connection.Open();
+        connection.FindWorklist(answers, *query);
+      }
+
+      Json::Value result;
+      answers.ToJson(result, true);
+      call.GetOutput().AnswerJson(result);
+    }
+  }
+
+
   void OrthancRestApi::RegisterModalities()
   {
     Register("/modalities", ListModalities);
@@ -857,6 +1038,7 @@
     Register("/modalities/{id}/find-instance", DicomFindInstance);
     Register("/modalities/{id}/find", DicomFind);
     Register("/modalities/{id}/store", DicomStore);
+    Register("/modalities/{id}/move", DicomMove);
 
     // For Query/Retrieve
     Register("/modalities/{id}/query", DicomQuery);
@@ -877,5 +1059,7 @@
     Register("/peers/{id}", UpdatePeer);
     Register("/peers/{id}", DeletePeer);
     Register("/peers/{id}/store", PeerStore);
+
+    Register("/modalities/{id}/find-worklist", DicomFindWorklist);
   }
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,15 +34,80 @@
 #include "../PrecompiledHeadersServer.h"
 #include "OrthancRestApi.h"
 
+#include "../../Core/Compression/GzipCompressor.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/DicomParsing/Internals/DicomImageDecoder.h"
+#include "../../Core/HttpServer/HttpContentNegociation.h"
 #include "../../Core/Logging.h"
+#include "../OrthancInitialization.h"
+#include "../Search/LookupResource.h"
+#include "../ServerContext.h"
 #include "../ServerToolbox.h"
-#include "../FromDcmtkBridge.h"
-#include "../ServerContext.h"
 #include "../SliceOrdering.h"
 
 
 namespace Orthanc
 {
+  static void AnswerDicomAsJson(RestApiCall& call,
+                                const Json::Value& dicom,
+                                DicomToJsonFormat mode)
+  {
+    if (mode != DicomToJsonFormat_Full)
+    {
+      Json::Value simplified;
+      ServerToolbox::SimplifyTags(simplified, dicom, mode);
+      call.GetOutput().AnswerJson(simplified);
+    }
+    else
+    {
+      call.GetOutput().AnswerJson(dicom);
+    }
+  }
+
+
+  static DicomToJsonFormat GetDicomFormat(const RestApiGetCall& call)
+  {
+    if (call.HasArgument("simplify"))
+    {
+      return DicomToJsonFormat_Human;
+    }
+    else if (call.HasArgument("short"))
+    {
+      return DicomToJsonFormat_Short;
+    }
+    else
+    {
+      return DicomToJsonFormat_Full;
+    }
+  }
+
+
+  static void AnswerDicomAsJson(RestApiGetCall& call,
+                                const Json::Value& dicom)
+  {
+    AnswerDicomAsJson(call, dicom, GetDicomFormat(call));
+  }
+
+
+  static void ParseSetOfTags(std::set<DicomTag>& target,
+                             const RestApiGetCall& call,
+                             const std::string& argument)
+  {
+    target.clear();
+
+    if (call.HasArgument(argument))
+    {
+      std::vector<std::string> tags;
+      Toolbox::TokenizeString(tags, call.GetArgument(argument, ""), ',');
+
+      for (size_t i = 0; i < tags.size(); i++)
+      {
+        target.insert(FromDcmtkBridge::ParseTag(tags[i]));
+      }
+    }
+  }
+
+
   // List all the patients, studies, series or instances ----------------------
  
   static void AnswerListOfResources(RestApiOutput& output,
@@ -184,78 +250,217 @@
     std::string publicId = call.GetUriComponent("id", "");
 
     std::string dicom;
-    context.ReadFile(dicom, publicId, FileContentType_Dicom);
+    context.ReadDicom(dicom, publicId);
 
     std::string target;
     call.BodyToString(target);
-    Toolbox::WriteFile(dicom, target);
+    SystemToolbox::WriteFile(dicom, target);
 
     call.GetOutput().AnswerBuffer("{}", "application/json");
   }
 
 
-  template <bool simplify>
+  template <DicomToJsonFormat format>
   static void GetInstanceTags(RestApiGetCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::string publicId = call.GetUriComponent("id", "");
+
+    std::set<DicomTag> ignoreTagLength;
+    ParseSetOfTags(ignoreTagLength, call, "ignore-length");
     
-    if (simplify)
+    if (format != DicomToJsonFormat_Full ||
+        !ignoreTagLength.empty())
     {
       Json::Value full;
-      context.ReadJson(full, publicId);
-
-      Json::Value simplified;
-      Toolbox::SimplifyTags(simplified, full);
-      call.GetOutput().AnswerJson(simplified);
+      context.ReadDicomAsJson(full, publicId, ignoreTagLength);
+      AnswerDicomAsJson(call, full, format);
     }
     else
     {
-      context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_DicomAsJson);
+      // This path allows to avoid the JSON decoding if no
+      // simplification is asked, and if no "ignore-length" argument
+      // is present
+      std::string full;
+      context.ReadDicomAsJson(full, publicId);
+      call.GetOutput().AnswerBuffer(full, "application/json");
     }
   }
 
 
   static void GetInstanceTagsBis(RestApiGetCall& call)
   {
-    bool simplify = call.HasArgument("simplify");
-
-    if (simplify)
+    switch (GetDicomFormat(call))
     {
-      GetInstanceTags<true>(call);
-    }
-    else
-    {
-      GetInstanceTags<false>(call);
+      case DicomToJsonFormat_Human:
+        GetInstanceTags<DicomToJsonFormat_Human>(call);
+        break;
+
+      case DicomToJsonFormat_Short:
+        GetInstanceTags<DicomToJsonFormat_Short>(call);
+        break;
+
+      case DicomToJsonFormat_Full:
+        GetInstanceTags<DicomToJsonFormat_Full>(call);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
     }
   }
 
   
   static void ListFrames(RestApiGetCall& call)
   {
-    Json::Value instance;
-    if (OrthancRestApi::GetIndex(call).LookupResource(instance, call.GetUriComponent("id", ""), ResourceType_Instance))
+    std::string publicId = call.GetUriComponent("id", "");
+
+    unsigned int numberOfFrames;
+      
     {
-      unsigned int numberOfFrames = 1;
+      ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
+      numberOfFrames = locker.GetDicom().GetFramesCount();
+    }
+    
+    Json::Value result = Json::arrayValue;
+    for (unsigned int i = 0; i < numberOfFrames; i++)
+    {
+      result.append(i);
+    }
+    
+    call.GetOutput().AnswerJson(result);
+  }
+
 
-      try
-      {
-        Json::Value tmp = instance["MainDicomTags"]["NumberOfFrames"];
-        numberOfFrames = boost::lexical_cast<unsigned int>(tmp.asString());
-      }
-      catch (...)
+  namespace
+  {
+    class ImageToEncode
+    {
+    private:
+      std::auto_ptr<ImageAccessor>&  image_;
+      ImageExtractionMode            mode_;
+      bool                           invert_;
+      std::string                    format_;
+      std::string                    answer_;
+
+    public:
+      ImageToEncode(std::auto_ptr<ImageAccessor>& image,
+                    ImageExtractionMode mode,
+                    bool invert) :
+        image_(image),
+        mode_(mode),
+        invert_(invert)
       {
       }
 
-      Json::Value result = Json::arrayValue;
-      for (unsigned int i = 0; i < numberOfFrames; i++)
+      void Answer(RestApiOutput& output)
+      {
+        output.AnswerBuffer(answer_, format_);
+      }
+
+      void EncodeUsingPng()
+      {
+        format_ = "image/png";
+        DicomImageDecoder::ExtractPngImage(answer_, image_, mode_, invert_);
+      }
+
+      void EncodeUsingPam()
       {
-        result.append(i);
+        /**
+         * "No Internet Media Type (aka MIME type, content type) for
+         * PBM has been registered with IANA, but the unofficial value
+         * image/x-portable-arbitrarymap is assigned by this
+         * specification, to be consistent with conventional values
+         * for the older Netpbm formats."
+         * http://netpbm.sourceforge.net/doc/pam.html
+         **/
+        format_ = "image/x-portable-arbitrarymap";
+        DicomImageDecoder::ExtractPamImage(answer_, image_, mode_, invert_);
+      }
+
+      void EncodeUsingJpeg(uint8_t quality)
+      {
+        format_ = "image/jpeg";
+        DicomImageDecoder::ExtractJpegImage(answer_, image_, mode_, invert_, quality);
+      }
+    };
+
+    class EncodePng : public HttpContentNegociation::IHandler
+    {
+    private:
+      ImageToEncode&  image_;
+
+    public:
+      EncodePng(ImageToEncode& image) : image_(image)
+      {
       }
 
-      call.GetOutput().AnswerJson(result);
-    }
+      virtual void Handle(const std::string& type,
+                          const std::string& subtype)
+      {
+        assert(type == "image");
+        assert(subtype == "png");
+        image_.EncodeUsingPng();
+      }
+    };
+
+    class EncodePam : public HttpContentNegociation::IHandler
+    {
+    private:
+      ImageToEncode&  image_;
+
+    public:
+      EncodePam(ImageToEncode& image) : image_(image)
+      {
+      }
+
+      virtual void Handle(const std::string& type,
+                          const std::string& subtype)
+      {
+        assert(type == "image");
+        assert(subtype == "x-portable-arbitrarymap");
+        image_.EncodeUsingPam();
+      }
+    };
+
+    class EncodeJpeg : public HttpContentNegociation::IHandler
+    {
+    private:
+      ImageToEncode&  image_;
+      unsigned int    quality_;
+
+    public:
+      EncodeJpeg(ImageToEncode& image,
+                 const RestApiGetCall& call) :
+        image_(image)
+      {
+        std::string v = call.GetArgument("quality", "90" /* default JPEG quality */);
+        bool ok = false;
+
+        try
+        {
+          quality_ = boost::lexical_cast<unsigned int>(v);
+          ok = (quality_ >= 1 && quality_ <= 100);
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+        }
+
+        if (!ok)
+        {
+          LOG(ERROR) << "Bad quality for a JPEG encoding (must be a number between 0 and 100): " << v;
+          throw OrthancException(ErrorCode_BadRequest);
+        }
+      }
+
+      virtual void Handle(const std::string& type,
+                          const std::string& subtype)
+      {
+        assert(type == "image");
+        assert(subtype == "jpeg");
+        image_.EncodeUsingJpeg(quality_);
+      }
+    };
   }
 
 
@@ -271,21 +476,64 @@
     {
       frame = boost::lexical_cast<unsigned int>(frameId);
     }
-    catch (boost::bad_lexical_cast)
+    catch (boost::bad_lexical_cast&)
     {
       return;
     }
 
-    std::string publicId = call.GetUriComponent("id", "");
-    std::string dicomContent, png;
-    context.ReadFile(dicomContent, publicId, FileContentType_Dicom);
-
-    ParsedDicomFile dicom(dicomContent);
+    bool invert = false;
+    std::auto_ptr<ImageAccessor> decoded;
 
     try
     {
-      dicom.ExtractPngImage(png, frame, mode);
-      call.GetOutput().AnswerBuffer(png, "image/png");
+      std::string publicId = call.GetUriComponent("id", "");
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+      if (context.GetPlugins().HasCustomImageDecoder())
+      {
+        // TODO create a cache of file
+        std::string dicomContent;
+        context.ReadDicom(dicomContent, publicId);
+        decoded.reset(context.GetPlugins().DecodeUnsafe(dicomContent.c_str(), dicomContent.size(), frame));
+
+        /**
+         * Note that we call "DecodeUnsafe()": We do not fallback to
+         * the builtin decoder if no installed decoder plugin is able
+         * to decode the image. This allows us to take advantage of
+         * the cache below.
+         **/
+
+        if (mode == ImageExtractionMode_Preview &&
+            decoded.get() != NULL)
+        {
+          // TODO Optimize this lookup for photometric interpretation:
+          // It should be implemented by the plugin to avoid parsing
+          // twice the DICOM file
+          ParsedDicomFile parsed(dicomContent);
+          
+          PhotometricInterpretation photometric;
+          if (parsed.LookupPhotometricInterpretation(photometric))
+          {
+            invert = (photometric == PhotometricInterpretation_Monochrome1);
+          }
+        }
+      }
+#endif
+
+      if (decoded.get() == NULL)
+      {
+        // Use Orthanc's built-in decoder, using the cache to speed-up
+        // things on multi-frame images
+        ServerContext::DicomCacheLocker locker(context, publicId);        
+        decoded.reset(DicomImageDecoder::Decode(locker.GetDicom(), frame));
+
+        PhotometricInterpretation photometric;
+        if (mode == ImageExtractionMode_Preview &&
+            locker.GetDicom().LookupPhotometricInterpretation(photometric))
+        {
+          invert = (photometric == PhotometricInterpretation_Monochrome1);
+        }
+      }
     }
     catch (OrthancException& e)
     {
@@ -305,6 +553,23 @@
         call.GetOutput().Redirect(root + "app/images/unsupported.png");
       }
     }
+
+    ImageToEncode image(decoded, mode, invert);
+
+    HttpContentNegociation negociation;
+    EncodePng png(image);
+    negociation.Register("image/png", png);
+
+    EncodeJpeg jpeg(image, call);
+    negociation.Register("image/jpeg", jpeg);
+
+    EncodePam pam(image);
+    negociation.Register("image/x-portable-arbitrarymap", pam);
+
+    if (negociation.Apply(call.GetHttpHeaders()))
+    {
+      image.Answer(call.GetOutput());
+    }
   }
 
 
@@ -319,28 +584,67 @@
     {
       frame = boost::lexical_cast<unsigned int>(frameId);
     }
-    catch (boost::bad_lexical_cast)
+    catch (boost::bad_lexical_cast&)
     {
       return;
     }
 
     std::string publicId = call.GetUriComponent("id", "");
     std::string dicomContent;
-    context.ReadFile(dicomContent, publicId, FileContentType_Dicom);
+    context.ReadDicom(dicomContent, publicId);
 
-    ParsedDicomFile dicom(dicomContent);
-    ImageBuffer buffer;
-    dicom.ExtractImage(buffer, frame);
+#if ORTHANC_ENABLE_PLUGINS == 1
+    IDicomImageDecoder& decoder = context.GetPlugins();
+#else
+    DefaultDicomImageDecoder decoder;  // This is Orthanc's built-in decoder
+#endif
 
-    ImageAccessor accessor(buffer.GetConstAccessor());
+    std::auto_ptr<ImageAccessor> decoded(decoder.Decode(dicomContent.c_str(), dicomContent.size(), frame));
 
     std::string result;
-    accessor.ToMatlabString(result);
+    decoded->ToMatlabString(result);
 
     call.GetOutput().AnswerBuffer(result, "text/plain");
   }
 
 
+  template <bool GzipCompression>
+  static void GetRawFrame(RestApiGetCall& call)
+  {
+    std::string frameId = call.GetUriComponent("frame", "0");
+
+    unsigned int frame;
+    try
+    {
+      frame = boost::lexical_cast<unsigned int>(frameId);
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      return;
+    }
+
+    std::string publicId = call.GetUriComponent("id", "");
+    std::string raw, mime;
+
+    {
+      ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
+      locker.GetDicom().GetRawFrame(raw, mime, frame);
+    }
+
+    if (GzipCompression)
+    {
+      GzipCompressor gzip;
+      std::string compressed;
+      gzip.Compress(compressed, raw.empty() ? NULL : raw.c_str(), raw.size());
+      call.GetOutput().AnswerBuffer(compressed, "application/gzip");
+    }
+    else
+    {
+      call.GetOutput().AnswerBuffer(raw, mime);
+    }
+  }
+
+
 
   static void GetResourceStatistics(RestApiGetCall& call)
   {
@@ -369,12 +673,33 @@
     std::list<MetadataType> metadata;
 
     OrthancRestApi::GetIndex(call).ListAvailableMetadata(metadata, publicId);
-    Json::Value result = Json::arrayValue;
+
+    Json::Value result;
 
-    for (std::list<MetadataType>::const_iterator 
-           it = metadata.begin(); it != metadata.end(); ++it)
+    if (call.HasArgument("expand"))
     {
-      result.append(EnumerationToString(*it));
+      result = Json::objectValue;
+      
+      for (std::list<MetadataType>::const_iterator 
+             it = metadata.begin(); it != metadata.end(); ++it)
+      {
+        std::string value;
+        if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, *it))
+        {
+          std::string key = EnumerationToString(*it);
+          result[key] = value;
+        }
+      }      
+    }
+    else
+    {
+      result = Json::arrayValue;
+      
+      for (std::list<MetadataType>::const_iterator 
+             it = metadata.begin(); it != metadata.end(); ++it)
+      {       
+        result.append(EnumerationToString(*it));
+      }
     }
 
     call.GetOutput().AnswerJson(result);
@@ -405,13 +730,15 @@
     std::string name = call.GetUriComponent("name", "");
     MetadataType metadata = StringToMetadata(name);
 
-    if (metadata >= MetadataType_StartUser &&
-        metadata <= MetadataType_EndUser)
-    {
-      // It is forbidden to modify internal metadata
+    if (IsUserMetadata(metadata))  // It is forbidden to modify internal metadata
+    {      
       OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata);
       call.GetOutput().AnswerBuffer("", "text/plain");
     }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
+    }
   }
 
 
@@ -426,13 +753,16 @@
     std::string value;
     call.BodyToString(value);
 
-    if (metadata >= MetadataType_StartUser &&
-        metadata <= MetadataType_EndUser)
+    if (IsUserMetadata(metadata))  // It is forbidden to modify internal metadata
     {
       // It is forbidden to modify internal metadata
       OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value);
       call.GetOutput().AnswerBuffer("", "text/plain");
     }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
+    }
   }
 
 
@@ -527,7 +857,7 @@
     {
       // Return the raw data (possibly compressed), as stored on the filesystem
       std::string content;
-      context.ReadFile(content, publicId, type, false);
+      context.ReadAttachment(content, publicId, type, false);
       call.GetOutput().AnswerBuffer(content, "application/octet-stream");
     }
   }
@@ -596,7 +926,7 @@
 
     // First check whether the compressed data is correctly stored in the disk
     std::string data;
-    context.ReadFile(data, publicId, StringToContentType(name), false);
+    context.ReadAttachment(data, publicId, StringToContentType(name), false);
 
     std::string actualMD5;
     Toolbox::ComputeMD5(actualMD5, data);
@@ -611,7 +941,7 @@
       }
       else
       {
-        context.ReadFile(data, publicId, StringToContentType(name), true);        
+        context.ReadAttachment(data, publicId, StringToContentType(name), true);        
         Toolbox::ComputeMD5(actualMD5, data);
         ok = (actualMD5 == info.GetUncompressedMD5());
       }
@@ -638,12 +968,15 @@
     std::string name = call.GetUriComponent("name", "");
 
     FileContentType contentType = StringToContentType(name);
-    if (contentType >= FileContentType_StartUser &&  // It is forbidden to modify internal attachments
-        contentType <= FileContentType_EndUser &&
+    if (IsUserContentType(contentType) &&  // It is forbidden to modify internal attachments
         context.AddAttachment(publicId, StringToContentType(name), call.GetBodyData(), call.GetBodySize()))
     {
       call.GetOutput().AnswerBuffer("{}", "application/json");
     }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
+    }
   }
 
 
@@ -655,13 +988,33 @@
     std::string name = call.GetUriComponent("name", "");
     FileContentType contentType = StringToContentType(name);
 
-    if (contentType >= FileContentType_StartUser &&
-        contentType <= FileContentType_EndUser)
+    bool allowed;
+    if (IsUserContentType(contentType))
+    {
+      allowed = true;
+    }
+    else if (Configuration::GetGlobalBoolParameter("StoreDicom", true) &&
+             contentType == FileContentType_DicomAsJson)
     {
-      // It is forbidden to delete internal attachments
+      allowed = true;
+    }
+    else
+    {
+      // It is forbidden to delete internal attachments, except for
+      // the "DICOM as JSON" summary as of Orthanc 1.2.0 (this summary
+      // would be automatically reconstructed on the next GET call)
+      allowed = false;
+    }
+
+    if (allowed) 
+    {
       OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType);
       call.GetOutput().AnswerBuffer("{}", "application/json");
     }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
+    }
   }
 
 
@@ -724,7 +1077,7 @@
 
       try
       {
-        context.ReadJson(tags, *it);
+        context.ReadDicomAsJson(tags, *it);
       }
       catch (OrthancException&)
       {
@@ -782,22 +1135,12 @@
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
     std::string publicId = call.GetUriComponent("id", "");
-    bool simplify = call.HasArgument("simplify");
 
     Json::Value sharedTags;
     if (ExtractSharedTags(sharedTags, context, publicId))
     {
       // Success: Send the value of the shared tags
-      if (simplify)
-      {
-        Json::Value simplified;
-        Toolbox::SimplifyTags(simplified, sharedTags);
-        call.GetOutput().AnswerJson(simplified);
-      }
-      else
-      {
-        call.GetOutput().AnswerJson(sharedTags);
-      }
+      AnswerDicomAsJson(call, sharedTags);
     }
   }
 
@@ -818,7 +1161,9 @@
 
     ServerContext& context = OrthancRestApi::GetContext(call);
     std::string publicId = call.GetUriComponent("id", "");
-    bool simplify = call.HasArgument("simplify");
+
+    std::set<DicomTag> ignoreTagLength;
+    ParseSetOfTags(ignoreTagLength, call, "ignore-length");
 
     typedef std::set<DicomTag> ModuleTags;
     ModuleTags moduleTags;
@@ -842,7 +1187,7 @@
       publicId = instances.front();
     }
 
-    context.ReadJson(tags, publicId);
+    context.ReadDicomAsJson(tags, publicId, ignoreTagLength);
     
     // Filter the tags of the instance according to the module
     Json::Value result = Json::objectValue;
@@ -855,16 +1200,7 @@
       }      
     }
 
-    if (simplify)
-    {
-      Json::Value simplified;
-      Toolbox::SimplifyTags(simplified, result);
-      call.GetOutput().AnswerJson(simplified);
-    }
-    else
-    {
-      call.GetOutput().AnswerJson(result);
-    }    
+    AnswerDicomAsJson(call, result);
   }
     
 
@@ -943,7 +1279,8 @@
         request["Level"].type() == Json::stringValue &&
         request["Query"].type() == Json::objectValue &&
         (!request.isMember("CaseSensitive") || request["CaseSensitive"].type() == Json::booleanValue) &&
-        (!request.isMember("Limit") || request["Limit"].type() == Json::intValue))
+        (!request.isMember("Limit") || request["Limit"].type() == Json::intValue) &&
+        (!request.isMember("Since") || request["Since"].type() == Json::intValue))
     {
       bool expand = false;
       if (request.isMember("Expand"))
@@ -960,11 +1297,25 @@
       size_t limit = 0;
       if (request.isMember("Limit"))
       {
-        limit = request["CaseSensitive"].asInt();
-        if (limit < 0)
+        int tmp = request["Limit"].asInt();
+        if (tmp < 0)
         {
           throw OrthancException(ErrorCode_ParameterOutOfRange);
         }
+
+        limit = static_cast<size_t>(tmp);
+      }
+
+      size_t since = 0;
+      if (request.isMember("Since"))
+      {
+        int tmp = request["Since"].asInt();
+        if (tmp < 0)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+
+        since = static_cast<size_t>(tmp);
       }
 
       std::string level = request["Level"].asString();
@@ -983,10 +1334,12 @@
                                  request["Query"][members[i]].asString(),
                                  caseSensitive);
       }
-      
+
+      bool isComplete;
       std::list<std::string> resources;
-      context.Apply(resources, query, limit);
-      AnswerListOfResources(call.GetOutput(), context.GetIndex(), resources, query.GetLevel(), expand);
+      context.Apply(isComplete, resources, query, since, limit);
+      AnswerListOfResources(call.GetOutput(), context.GetIndex(),
+                            resources, query.GetLevel(), expand);
     }
     else
     {
@@ -1043,7 +1396,10 @@
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
     std::string publicId = call.GetUriComponent("id", "");
-    bool simplify = call.HasArgument("simplify");
+    DicomToJsonFormat format = GetDicomFormat(call);
+
+    std::set<DicomTag> ignoreTagLength;
+    ParseSetOfTags(ignoreTagLength, call, "ignore-length");
 
     // Retrieve all the instances of this patient/study/series
     typedef std::list<std::string> Instances;
@@ -1057,12 +1413,12 @@
          it != instances.end(); ++it)
     {
       Json::Value full;
-      context.ReadJson(full, *it);
+      context.ReadDicomAsJson(full, *it, ignoreTagLength);
 
-      if (simplify)
+      if (format != DicomToJsonFormat_Full)
       {
         Json::Value simplified;
-        Toolbox::SimplifyTags(simplified, full);
+        ServerToolbox::SimplifyTags(simplified, full, format);
         result[*it] = simplified;
       }
       else
@@ -1138,6 +1494,79 @@
   }
 
 
+  static void GetInstanceHeader(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::string publicId = call.GetUriComponent("id", "");
+
+    std::string dicomContent;
+    context.ReadDicom(dicomContent, publicId);
+
+    // TODO Consider using "DicomMap::ParseDicomMetaInformation()" to
+    // speed up things here
+
+    ParsedDicomFile dicom(dicomContent);
+
+    Json::Value header;
+    dicom.HeaderToJson(header, DicomToJsonFormat_Full);
+
+    AnswerDicomAsJson(call, header);
+  }
+
+
+  static void InvalidateTags(RestApiPostCall& call)
+  {
+    ServerIndex& index = OrthancRestApi::GetIndex(call);
+    
+    // Loop over the instances, grouping them by parent studies so as
+    // to avoid large memory consumption
+    std::list<std::string> studies;
+    index.GetAllUuids(studies, ResourceType_Study);
+
+    for (std::list<std::string>::const_iterator 
+           study = studies.begin(); study != studies.end(); ++study)
+    {
+      std::list<std::string> instances;
+      index.GetChildInstances(instances, *study);
+
+      for (std::list<std::string>::const_iterator 
+             instance = instances.begin(); instance != instances.end(); ++instance)
+      {
+        index.DeleteAttachment(*instance, FileContentType_DicomAsJson);
+      }
+    }
+
+    call.GetOutput().AnswerBuffer("", "text/plain");
+  }
+
+
+  template <enum ResourceType type>
+  static void ReconstructResource(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", ""));
+    call.GetOutput().AnswerBuffer("", "text/plain");
+  }
+
+
+  static void ReconstructAllResources(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::list<std::string> studies;
+    context.GetIndex().GetAllUuids(studies, ResourceType_Study);
+
+    for (std::list<std::string>::const_iterator 
+           study = studies.begin(); study != studies.end(); ++study)
+    {
+      ServerToolbox::ReconstructResource(context, *study);
+    }
+    
+    call.GetOutput().AnswerBuffer("", "text/plain");
+  }
+
+
   void OrthancRestApi::RegisterResources()
   {
     Register("/instances", ListResources<ResourceType_Instance>);
@@ -1172,7 +1601,7 @@
     Register("/instances/{id}/file", GetInstanceFile);
     Register("/instances/{id}/export", ExportInstanceFile);
     Register("/instances/{id}/tags", GetInstanceTagsBis);
-    Register("/instances/{id}/simplified-tags", GetInstanceTags<true>);
+    Register("/instances/{id}/simplified-tags", GetInstanceTags<DicomToJsonFormat_Human>);
     Register("/instances/{id}/frames", ListFrames);
 
     Register("/instances/{id}/frames/{frame}/preview", GetImage<ImageExtractionMode_Preview>);
@@ -1180,12 +1609,15 @@
     Register("/instances/{id}/frames/{frame}/image-uint16", GetImage<ImageExtractionMode_UInt16>);
     Register("/instances/{id}/frames/{frame}/image-int16", GetImage<ImageExtractionMode_Int16>);
     Register("/instances/{id}/frames/{frame}/matlab", GetMatlabImage);
+    Register("/instances/{id}/frames/{frame}/raw", GetRawFrame<false>);
+    Register("/instances/{id}/frames/{frame}/raw.gz", GetRawFrame<true>);
     Register("/instances/{id}/pdf", ExtractPdf);
     Register("/instances/{id}/preview", GetImage<ImageExtractionMode_Preview>);
     Register("/instances/{id}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
     Register("/instances/{id}/image-uint16", GetImage<ImageExtractionMode_UInt16>);
     Register("/instances/{id}/image-int16", GetImage<ImageExtractionMode_Int16>);
     Register("/instances/{id}/matlab", GetMatlabImage);
+    Register("/instances/{id}/header", GetInstanceHeader);
 
     Register("/patients/{id}/protected", IsProtectedPatient);
     Register("/patients/{id}/protected", SetPatientProtection);
@@ -1210,6 +1642,7 @@
     Register("/{resourceType}/{id}/attachments/{name}/uncompress", ChangeAttachmentCompression<CompressionType_None>);
     Register("/{resourceType}/{id}/attachments/{name}/verify-md5", VerifyAttachment);
 
+    Register("/tools/invalidate-tags", InvalidateTags);
     Register("/tools/lookup", Lookup);
     Register("/tools/find", Find);
 
@@ -1234,5 +1667,11 @@
     Register("/instances/{id}/content/*", GetRawContent);
 
     Register("/series/{id}/ordered-slices", OrderSlices);
+
+    Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>);
+    Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>);
+    Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>);
+    Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>);
+    Register("/tools/reconstruct", ReconstructAllResources);
   }
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,7 +35,7 @@
 #include "OrthancRestApi.h"
 
 #include "../OrthancInitialization.h"
-#include "../FromDcmtkBridge.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Plugins/Engine/PluginsManager.h"
 #include "../../Plugins/Engine/OrthancPlugins.h"
 #include "../ServerContext.h"
@@ -53,17 +54,18 @@
   {
     Json::Value result = Json::objectValue;
 
+    result["ApiVersion"] = ORTHANC_API_VERSION;
     result["DatabaseVersion"] = OrthancRestApi::GetIndex(call).GetDatabaseVersion();
     result["DicomAet"] = Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC");
-    result["DicomPort"] = Configuration::GetGlobalIntegerParameter("DicomPort", 4242);
-    result["HttpPort"] = Configuration::GetGlobalIntegerParameter("HttpPort", 8042);
+    result["DicomPort"] = Configuration::GetGlobalUnsignedIntegerParameter("DicomPort", 4242);
+    result["HttpPort"] = Configuration::GetGlobalUnsignedIntegerParameter("HttpPort", 8042);
     result["Name"] = Configuration::GetGlobalStringParameter("Name", "");
     result["Version"] = ORTHANC_VERSION;
 
     result["StorageAreaPlugin"] = Json::nullValue;
     result["DatabaseBackendPlugin"] = Json::nullValue;
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
     result["PluginsEnabled"] = true;
     const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetPlugins();
 
@@ -122,16 +124,17 @@
     call.BodyToString(command);
 
     {
-      LuaScripting::Locker locker(context.GetLua());
-      locker.GetLua().Execute(result, command);
+      LuaScripting::Lock lock(context.GetLuaScripting());
+      lock.GetLua().Execute(result, command);
     }
 
     call.GetOutput().AnswerBuffer(result, "text/plain");
   }
 
+  template <bool UTC>
   static void GetNowIsoString(RestApiGetCall& call)
   {
-    call.GetOutput().AnswerBuffer(Toolbox::GetNowIsoString(), "text/plain");
+    call.GetOutput().AnswerBuffer(SystemToolbox::GetNowIsoString(UTC), "text/plain");
   }
 
 
@@ -143,6 +146,24 @@
   }
 
 
+  static void GetDefaultEncoding(RestApiGetCall& call)
+  {
+    Encoding encoding = GetDefaultDicomEncoding();
+    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain");
+  }
+
+
+  static void SetDefaultEncoding(RestApiPutCall& call)
+  {
+    Encoding encoding = StringToEncoding(call.GetBodyData());
+
+    Configuration::SetDefaultEncoding(encoding);
+
+    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain");
+  }
+
+
+  
   // Plugins information ------------------------------------------------------
 
   static void ListPlugins(RestApiGetCall& call)
@@ -153,7 +174,7 @@
 
     if (OrthancRestApi::GetContext(call).HasPlugins())
     {
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
       std::list<std::string> plugins;
       OrthancRestApi::GetContext(call).GetPlugins().GetManager().ListPlugins(plugins);
 
@@ -176,7 +197,7 @@
       return;
     }
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
     const PluginsManager& manager = OrthancRestApi::GetContext(call).GetPlugins().GetManager();
     std::string id = call.GetUriComponent("id", "");
 
@@ -224,7 +245,7 @@
 
     if (OrthancRestApi::GetContext(call).HasPlugins())
     {
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
       const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetPlugins();
       const PluginsManager& manager = plugins.GetManager();
 
@@ -248,6 +269,99 @@
   }
 
 
+
+
+  // Jobs information ------------------------------------------------------
+
+  static void ListJobs(RestApiGetCall& call)
+  {
+    bool expand = call.HasArgument("expand");
+
+    Json::Value v = Json::arrayValue;
+
+    std::set<std::string> jobs;
+    OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().ListJobs(jobs);
+
+    for (std::set<std::string>::const_iterator it = jobs.begin();
+         it != jobs.end(); ++it)
+    {
+      if (expand)
+      {
+        JobInfo info;
+        if (OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().GetJobInfo(info, *it))
+        {
+          Json::Value tmp;
+          info.Format(tmp);
+          v.append(tmp);
+        }
+      }
+      else
+      {
+        v.append(*it);
+      }
+    }
+    
+    call.GetOutput().AnswerJson(v);
+  }
+
+  static void GetJobInfo(RestApiGetCall& call)
+  {
+    std::string id = call.GetUriComponent("id", "");
+
+    JobInfo info;
+    if (OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().GetJobInfo(info, id))
+    {
+      Json::Value json;
+      info.Format(json);
+      call.GetOutput().AnswerJson(json);
+    }
+  }
+
+
+  enum JobAction
+  {
+    JobAction_Cancel,
+    JobAction_Pause,
+    JobAction_Resubmit,
+    JobAction_Resume
+  };
+
+  template <JobAction action>
+  static void ApplyJobAction(RestApiPostCall& call)
+  {
+    std::string id = call.GetUriComponent("id", "");
+
+    bool ok = false;
+
+    switch (action)
+    {
+      case JobAction_Cancel:
+        ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Cancel(id);
+        break;
+
+      case JobAction_Pause:
+        ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Pause(id);
+        break;
+ 
+      case JobAction_Resubmit:
+        ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Resubmit(id);
+        break;
+
+      case JobAction_Resume:
+        ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Resume(id);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    if (ok)
+    {
+      call.GetOutput().AnswerBuffer("{}", "application/json");
+    }
+  }
+
+  
   void OrthancRestApi::RegisterSystem()
   {
     Register("/", ServeRoot);
@@ -255,11 +369,21 @@
     Register("/statistics", GetStatistics);
     Register("/tools/generate-uid", GenerateUid);
     Register("/tools/execute-script", ExecuteScript);
-    Register("/tools/now", GetNowIsoString);
+    Register("/tools/now", GetNowIsoString<true>);
+    Register("/tools/now-local", GetNowIsoString<false>);
     Register("/tools/dicom-conformance", GetDicomConformanceStatement);
+    Register("/tools/default-encoding", GetDefaultEncoding);
+    Register("/tools/default-encoding", SetDefaultEncoding);
 
     Register("/plugins", ListPlugins);
     Register("/plugins/{id}", GetPlugin);
     Register("/plugins/explorer.js", GetOrthancExplorerPlugins);
+
+    Register("/jobs", ListJobs);
+    Register("/jobs/{id}", GetJobInfo);
+    Register("/jobs/{id}/cancel", ApplyJobAction<JobAction_Cancel>);
+    Register("/jobs/{id}/pause", ApplyJobAction<JobAction_Pause>);
+    Register("/jobs/{id}/resubmit", ApplyJobAction<JobAction_Resubmit>);
+    Register("/jobs/{id}/resume", ApplyJobAction<JobAction_Resume>);
   }
 }
--- a/OrthancServer/ParsedDicomFile.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1225 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-
-/*=========================================================================
-
-  This file is based on portions of the following project:
-
-  Program: GDCM (Grassroots DICOM). A DICOM library
-  Module:  http://gdcm.sourceforge.net/Copyright.html
-
-Copyright (c) 2006-2011 Mathieu Malaterre
-Copyright (c) 1993-2005 CREATIS
-(CREATIS = Centre de Recherche et d'Applications en Traitement de l'Image)
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
- * Redistributions of source code must retain the above copyright notice,
-   this list of conditions and the following disclaimer.
-
- * Redistributions in binary form must reproduce the above copyright notice,
-   this list of conditions and the following disclaimer in the documentation
-   and/or other materials provided with the distribution.
-
- * Neither name of Mathieu Malaterre, or CREATIS, nor the names of any
-   contributors (CNRS, INSERM, UCB, Universite Lyon I), may be used to
-   endorse or promote products derived from this software without specific
-   prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
-ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-=========================================================================*/
-
-
-#include "PrecompiledHeadersServer.h"
-
-#ifndef NOMINMAX
-#define NOMINMAX
-#endif
-
-#include "ParsedDicomFile.h"
-
-#include "ServerToolbox.h"
-#include "FromDcmtkBridge.h"
-#include "ToDcmtkBridge.h"
-#include "Internals/DicomImageDecoder.h"
-#include "../Core/Logging.h"
-#include "../Core/Toolbox.h"
-#include "../Core/OrthancException.h"
-#include "../Core/Images/ImageBuffer.h"
-#include "../Core/Images/PngWriter.h"
-#include "../Core/Uuid.h"
-#include "../Core/DicomFormat/DicomIntegerPixelAccessor.h"
-#include "../Core/Images/PngReader.h"
-
-#include <list>
-#include <limits>
-
-#include <boost/lexical_cast.hpp>
-
-#include <dcmtk/dcmdata/dcchrstr.h>
-#include <dcmtk/dcmdata/dcdicent.h>
-#include <dcmtk/dcmdata/dcdict.h>
-#include <dcmtk/dcmdata/dcfilefo.h>
-#include <dcmtk/dcmdata/dcistrmb.h>
-#include <dcmtk/dcmdata/dcuid.h>
-#include <dcmtk/dcmdata/dcmetinf.h>
-#include <dcmtk/dcmdata/dcdeftag.h>
-
-#include <dcmtk/dcmdata/dcvrae.h>
-#include <dcmtk/dcmdata/dcvras.h>
-#include <dcmtk/dcmdata/dcvrcs.h>
-#include <dcmtk/dcmdata/dcvrda.h>
-#include <dcmtk/dcmdata/dcvrds.h>
-#include <dcmtk/dcmdata/dcvrdt.h>
-#include <dcmtk/dcmdata/dcvrfd.h>
-#include <dcmtk/dcmdata/dcvrfl.h>
-#include <dcmtk/dcmdata/dcvris.h>
-#include <dcmtk/dcmdata/dcvrlo.h>
-#include <dcmtk/dcmdata/dcvrlt.h>
-#include <dcmtk/dcmdata/dcvrpn.h>
-#include <dcmtk/dcmdata/dcvrsh.h>
-#include <dcmtk/dcmdata/dcvrsl.h>
-#include <dcmtk/dcmdata/dcvrss.h>
-#include <dcmtk/dcmdata/dcvrst.h>
-#include <dcmtk/dcmdata/dcvrtm.h>
-#include <dcmtk/dcmdata/dcvrui.h>
-#include <dcmtk/dcmdata/dcvrul.h>
-#include <dcmtk/dcmdata/dcvrus.h>
-#include <dcmtk/dcmdata/dcvrut.h>
-#include <dcmtk/dcmdata/dcpixel.h>
-#include <dcmtk/dcmdata/dcpixseq.h>
-#include <dcmtk/dcmdata/dcpxitem.h>
-
-
-#include <boost/math/special_functions/round.hpp>
-#include <dcmtk/dcmdata/dcostrmb.h>
-#include <boost/algorithm/string/predicate.hpp>
-
-
-static const char* CONTENT_TYPE_OCTET_STREAM = "application/octet-stream";
-
-
-
-namespace Orthanc
-{
-  struct ParsedDicomFile::PImpl
-  {
-    std::auto_ptr<DcmFileFormat> file_;
-  };
-
-
-  // This method can only be called from the constructors!
-  void ParsedDicomFile::Setup(const char* buffer, size_t size)
-  {
-    DcmInputBufferStream is;
-    if (size > 0)
-    {
-      is.setBuffer(buffer, size);
-    }
-    is.setEos();
-
-    pimpl_->file_.reset(new DcmFileFormat);
-    pimpl_->file_->transferInit();
-    if (!pimpl_->file_->read(is).good())
-    {
-      delete pimpl_;  // Avoid a memory leak due to exception
-                      // throwing, as we are in the constructor
-
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-    pimpl_->file_->loadAllDataIntoMemory();
-    pimpl_->file_->transferEnd();
-  }
-
-
-  static void SendPathValueForDictionary(RestApiOutput& output,
-                                         DcmItem& dicom)
-  {
-    Json::Value v = Json::arrayValue;
-
-    for (unsigned long i = 0; i < dicom.card(); i++)
-    {
-      DcmElement* element = dicom.getElement(i);
-      if (element)
-      {
-        char buf[16];
-        sprintf(buf, "%04x-%04x", element->getTag().getGTag(), element->getTag().getETag());
-        v.append(buf);
-      }
-    }
-
-    output.AnswerJson(v);
-  }
-
-  static inline uint16_t GetCharValue(char c)
-  {
-    if (c >= '0' && c <= '9')
-      return c - '0';
-    else if (c >= 'a' && c <= 'f')
-      return c - 'a' + 10;
-    else if (c >= 'A' && c <= 'F')
-      return c - 'A' + 10;
-    else
-      return 0;
-  }
-
-  static inline uint16_t GetTagValue(const char* c)
-  {
-    return ((GetCharValue(c[0]) << 12) + 
-            (GetCharValue(c[1]) << 8) + 
-            (GetCharValue(c[2]) << 4) + 
-            GetCharValue(c[3]));
-  }
-
-  static void ParseTagAndGroup(DcmTagKey& key,
-                               const std::string& tag)
-  {
-    DicomTag t = FromDcmtkBridge::ParseTag(tag);
-    key = DcmTagKey(t.GetGroup(), t.GetElement());
-  }
-
-
-  static void SendSequence(RestApiOutput& output,
-                           DcmSequenceOfItems& sequence)
-  {
-    // This element is a sequence
-    Json::Value v = Json::arrayValue;
-
-    for (unsigned long i = 0; i < sequence.card(); i++)
-    {
-      v.append(boost::lexical_cast<std::string>(i));
-    }
-
-    output.AnswerJson(v);
-  }
-
-
-  static unsigned int GetPixelDataBlockCount(DcmPixelData& pixelData,
-                                             E_TransferSyntax transferSyntax)
-  {
-    DcmPixelSequence* pixelSequence = NULL;
-    if (pixelData.getEncapsulatedRepresentation
-        (transferSyntax, NULL, pixelSequence).good() && pixelSequence)
-    {
-      return pixelSequence->card();
-    }
-    else
-    {
-      return 1;
-    }
-  }
-
-
-  namespace
-  {
-    class DicomFieldStream : public IHttpStreamAnswer
-    {
-    private:
-      DcmElement&  element_;
-      uint32_t     length_;
-      uint32_t     offset_;
-      std::string  chunk_;
-      size_t       chunkSize_;
-      
-    public:
-      DicomFieldStream(DcmElement& element,
-                       E_TransferSyntax transferSyntax) :
-        element_(element),
-        length_(element.getLength(transferSyntax)),
-        offset_(0),
-        chunkSize_(0)
-      {
-        static const size_t CHUNK_SIZE = 64 * 1024;  // Use chunks of max 64KB
-        chunk_.resize(CHUNK_SIZE);
-      }
-
-      virtual HttpCompression SetupHttpCompression(bool /*gzipAllowed*/,
-                                                   bool /*deflateAllowed*/)
-      {
-        // No support for compression
-        return HttpCompression_None;
-      }
-
-      virtual bool HasContentFilename(std::string& filename)
-      {
-        return false;
-      }
-
-      virtual std::string GetContentType()
-      {
-        return "";
-      }
-
-      virtual uint64_t  GetContentLength()
-      {
-        return length_;
-      }
- 
-      virtual bool ReadNextChunk()
-      {
-        assert(offset_ <= length_);
-
-        if (offset_ == length_)
-        {
-          return false;
-        }
-        else
-        {
-          if (length_ - offset_ < chunk_.size())
-          {
-            chunkSize_ = length_ - offset_;
-          }
-          else
-          {
-            chunkSize_ = chunk_.size();
-          }
-
-          OFCondition cond = element_.getPartialValue(&chunk_[0], offset_, chunkSize_);
-
-          offset_ += chunkSize_;
-
-          if (!cond.good())
-          {
-            LOG(ERROR) << "Error while sending a DICOM field: " << cond.text();
-            throw OrthancException(ErrorCode_InternalError);
-          }
-
-          return true;
-        }
-      }
- 
-      virtual const char *GetChunkContent()
-      {
-        return chunk_.c_str();
-      }
- 
-      virtual size_t GetChunkSize()
-      {
-        return chunkSize_;
-      }
-    };
-  }
-
-
-  static bool AnswerPixelData(RestApiOutput& output,
-                              DcmItem& dicom,
-                              E_TransferSyntax transferSyntax,
-                              const std::string* blockUri)
-  {
-    DcmTag k(DICOM_TAG_PIXEL_DATA.GetGroup(),
-             DICOM_TAG_PIXEL_DATA.GetElement());
-
-    DcmElement *element = NULL;
-    if (!dicom.findAndGetElement(k, element).good() ||
-        element == NULL)
-    {
-      return false;
-    }
-
-    try
-    {
-      DcmPixelData& pixelData = dynamic_cast<DcmPixelData&>(*element);
-      if (blockUri == NULL)
-      {
-        // The user asks how many blocks are presents in this pixel data
-        unsigned int blocks = GetPixelDataBlockCount(pixelData, transferSyntax);
-
-        Json::Value result(Json::arrayValue);
-        for (unsigned int i = 0; i < blocks; i++)
-        {
-          result.append(boost::lexical_cast<std::string>(i));
-        }
-        
-        output.AnswerJson(result);
-        return true;
-      }
-
-
-      unsigned int block = boost::lexical_cast<unsigned int>(*blockUri);
-
-      if (block < GetPixelDataBlockCount(pixelData, transferSyntax))
-      {
-        DcmPixelSequence* pixelSequence = NULL;
-        if (pixelData.getEncapsulatedRepresentation
-            (transferSyntax, NULL, pixelSequence).good() && pixelSequence)
-        {
-          // This is the case for JPEG transfer syntaxes
-          if (block < pixelSequence->card())
-          {
-            DcmPixelItem* pixelItem = NULL;
-            if (pixelSequence->getItem(pixelItem, block).good() && pixelItem)
-            {
-              if (pixelItem->getLength() == 0)
-              {
-                output.AnswerBuffer(NULL, 0, CONTENT_TYPE_OCTET_STREAM);
-                return true;
-              }
-
-              Uint8* buffer = NULL;
-              if (pixelItem->getUint8Array(buffer).good() && buffer)
-              {
-                output.AnswerBuffer(buffer, pixelItem->getLength(), CONTENT_TYPE_OCTET_STREAM);
-                return true;
-              }
-            }
-          }
-        }
-        else
-        {
-          // This is the case for raw, uncompressed image buffers
-          assert(*blockUri == "0");
-          DicomFieldStream stream(*element, transferSyntax);
-          output.AnswerStream(stream);
-        }
-      }
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-      // The URI entered by the user is not a number
-    }
-    catch (std::bad_cast&)
-    {
-      // This should never happen
-    }
-
-    return false;
-  }
-
-
-
-  static void SendPathValueForLeaf(RestApiOutput& output,
-                                   const std::string& tag,
-                                   DcmItem& dicom,
-                                   E_TransferSyntax transferSyntax)
-  {
-    DcmTagKey k;
-    ParseTagAndGroup(k, tag);
-
-    DcmSequenceOfItems* sequence = NULL;
-    if (dicom.findAndGetSequence(k, sequence).good() && 
-        sequence != NULL &&
-        sequence->getVR() == EVR_SQ)
-    {
-      SendSequence(output, *sequence);
-      return;
-    }
-
-    DcmElement* element = NULL;
-    if (dicom.findAndGetElement(k, element).good() && 
-        element != NULL &&
-        //element->getVR() != EVR_UNKNOWN &&  // This would forbid private tags
-        element->getVR() != EVR_SQ)
-    {
-      DicomFieldStream stream(*element, transferSyntax);
-      output.AnswerStream(stream);
-    }
-  }
-
-  void ParsedDicomFile::SendPathValue(RestApiOutput& output,
-                                      const UriComponents& uri)
-  {
-    DcmItem* dicom = pimpl_->file_->getDataset();
-    E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getOriginalXfer();
-
-    // Special case: Accessing the pixel data
-    if (uri.size() == 1 || 
-        uri.size() == 2)
-    {
-      DcmTagKey tag;
-      ParseTagAndGroup(tag, uri[0]);
-
-      if (tag.getGroup() == DICOM_TAG_PIXEL_DATA.GetGroup() &&
-          tag.getElement() == DICOM_TAG_PIXEL_DATA.GetElement())
-      {
-        AnswerPixelData(output, *dicom, transferSyntax, uri.size() == 1 ? NULL : &uri[1]);
-        return;
-      }
-    }        
-
-    // Go down in the tag hierarchy according to the URI
-    for (size_t pos = 0; pos < uri.size() / 2; pos++)
-    {
-      size_t index;
-      try
-      {
-        index = boost::lexical_cast<size_t>(uri[2 * pos + 1]);
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        return;
-      }
-
-      DcmTagKey k;
-      DcmItem *child = NULL;
-      ParseTagAndGroup(k, uri[2 * pos]);
-      if (!dicom->findAndGetSequenceItem(k, child, index).good() ||
-          child == NULL)
-      {
-        return;
-      }
-
-      dicom = child;
-    }
-
-    // We have reached the end of the URI
-    if (uri.size() % 2 == 0)
-    {
-      SendPathValueForDictionary(output, *dicom);
-    }
-    else
-    {
-      SendPathValueForLeaf(output, uri.back(), *dicom, transferSyntax);
-    }
-  }
-
-
-  void ParsedDicomFile::Remove(const DicomTag& tag)
-  {
-    DcmTagKey key(tag.GetGroup(), tag.GetElement());
-    DcmElement* element = pimpl_->file_->getDataset()->remove(key);
-    if (element != NULL)
-    {
-      delete element;
-    }
-  }
-
-
-
-  void ParsedDicomFile::RemovePrivateTagsInternal(const std::set<DicomTag>* toKeep)
-  {
-    DcmDataset& dataset = *pimpl_->file_->getDataset();
-
-    // Loop over the dataset to detect its private tags
-    typedef std::list<DcmElement*> Tags;
-    Tags privateTags;
-
-    for (unsigned long i = 0; i < dataset.card(); i++)
-    {
-      DcmElement* element = dataset.getElement(i);
-      DcmTag tag(element->getTag());
-
-      // Is this a private tag?
-      if (tag.isPrivate())
-      {
-        bool remove = true;
-
-        // Check whether this private tag is to be kept
-        if (toKeep != NULL)
-        {
-          DicomTag tmp = FromDcmtkBridge::Convert(tag);
-          if (toKeep->find(tmp) != toKeep->end())
-          {
-            remove = false;  // Keep it
-          }
-        }
-            
-        if (remove)
-        {
-          privateTags.push_back(element);
-        }
-      }
-    }
-
-    // Loop over the detected private tags to remove them
-    for (Tags::iterator it = privateTags.begin(); 
-         it != privateTags.end(); ++it)
-    {
-      DcmElement* tmp = dataset.remove(*it);
-      if (tmp != NULL)
-      {
-        delete tmp;
-      }
-    }
-  }
-
-
-  static void InsertInternal(DcmDataset& dicom,
-                             DcmElement* element)
-  {
-    OFCondition cond = dicom.insert(element, false, false);
-    if (!cond.good())
-    {
-      // This field already exists
-      delete element;
-      throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-
-  void ParsedDicomFile::Insert(const DicomTag& tag,
-                               const Json::Value& value,
-                               bool decodeBinaryTags)
-  {
-    std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeBinaryTags, GetEncoding()));
-    InsertInternal(*pimpl_->file_->getDataset(), element.release());
-  }
-
-
-  static void ReplaceInternal(DcmDataset& dicom,
-                              std::auto_ptr<DcmElement>& element,
-                              DicomReplaceMode mode)
-  {
-    const DcmTagKey& tag = element->getTag();
-
-    if (!dicom.findAndDeleteElement(tag).good())
-    {
-      // This field does not exist, act wrt. the specified "mode"
-      switch (mode)
-      {
-        case DicomReplaceMode_InsertIfAbsent:
-          break;
-
-        case DicomReplaceMode_ThrowIfAbsent:
-          throw OrthancException(ErrorCode_InexistentItem);
-
-        case DicomReplaceMode_IgnoreIfAbsent:
-          return;
-      }
-    }
-
-    // Either the tag was not existing, or the replace mode was set to
-    // "InsertIfAbsent"
-    InsertInternal(dicom, element.release());
-  }
-
-
-  void ParsedDicomFile::UpdateStorageUid(const DicomTag& tag,
-                                         const std::string& utf8Value,
-                                         bool decodeBinaryTags)
-  {
-    if (tag != DICOM_TAG_SOP_CLASS_UID &&
-        tag != DICOM_TAG_SOP_INSTANCE_UID)
-    {
-      return;
-    }
-
-    std::string binary;
-    const std::string* decoded = &utf8Value;
-
-    if (decodeBinaryTags &&
-        boost::starts_with(utf8Value, "data:application/octet-stream;base64,"))
-    {
-      std::string mime;
-      Toolbox::DecodeDataUriScheme(mime, binary, utf8Value);
-      decoded = &binary;
-    }
-    else
-    {
-      Encoding encoding = GetEncoding();
-      if (GetEncoding() != Encoding_Utf8)
-      {
-        binary = Toolbox::ConvertFromUtf8(utf8Value, encoding);
-        decoded = &binary;
-      }
-    }
-
-    /**
-     * dcmodify will automatically correct 'Media Storage SOP Class
-     * UID' and 'Media Storage SOP Instance UID' in the metaheader, if
-     * you make changes to the related tags in the dataset ('SOP Class
-     * UID' and 'SOP Instance UID') via insert or modify mode
-     * options. You can disable this behaviour by using the -nmu
-     * option.
-     **/
-
-    if (tag == DICOM_TAG_SOP_CLASS_UID)
-    {
-      Replace(DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID, *decoded, DicomReplaceMode_InsertIfAbsent);
-    }
-
-    if (tag == DICOM_TAG_SOP_INSTANCE_UID)
-    {
-      Replace(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID, *decoded, DicomReplaceMode_InsertIfAbsent);
-    }    
-  }
-
-
-  void ParsedDicomFile::Replace(const DicomTag& tag,
-                                const std::string& utf8Value,
-                                DicomReplaceMode mode)
-  {
-    std::auto_ptr<DcmElement> element(FromDcmtkBridge::CreateElementForTag(tag));
-    FromDcmtkBridge::FillElementWithString(*element, tag, utf8Value, false, GetEncoding());
-    ReplaceInternal(*pimpl_->file_->getDataset(), element, mode);
-    UpdateStorageUid(tag, utf8Value, false);
-  }
-
-    
-  void ParsedDicomFile::Replace(const DicomTag& tag,
-                                const Json::Value& value,
-                                bool decodeBinaryTags,
-                                DicomReplaceMode mode)
-  {
-    std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeBinaryTags, GetEncoding()));
-    ReplaceInternal(*pimpl_->file_->getDataset(), element, mode);
-
-    if (tag == DICOM_TAG_SOP_CLASS_UID ||
-        tag == DICOM_TAG_SOP_INSTANCE_UID)
-    {
-      if (value.type() != Json::stringValue)
-      {
-        throw OrthancException(ErrorCode_BadParameterType);
-      }
-
-      UpdateStorageUid(tag, value.asString(), decodeBinaryTags);
-    }
-  }
-
-    
-  void ParsedDicomFile::Answer(RestApiOutput& output)
-  {
-    std::string serialized;
-    if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *pimpl_->file_->getDataset()))
-    {
-      output.AnswerBuffer(serialized, CONTENT_TYPE_OCTET_STREAM);
-    }
-  }
-
-
-
-  bool ParsedDicomFile::GetTagValue(std::string& value,
-                                    const DicomTag& tag)
-  {
-    DcmTagKey k(tag.GetGroup(), tag.GetElement());
-    DcmDataset& dataset = *pimpl_->file_->getDataset();
-
-    if (FromDcmtkBridge::IsPrivateTag(tag) ||
-        FromDcmtkBridge::IsUnknownTag(tag) ||
-        tag == DICOM_TAG_PIXEL_DATA ||
-        tag == DICOM_TAG_ENCAPSULATED_DOCUMENT)
-    {
-      const Uint8* data = NULL;   // This is freed in the destructor of the dataset
-      long unsigned int count = 0;
-
-      if (dataset.findAndGetUint8Array(k, data, &count).good())
-      {
-        if (count > 0)
-        {
-          assert(data != NULL);
-          value.assign(reinterpret_cast<const char*>(data), count);
-        }
-        else
-        {
-          value.clear();
-        }
-
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-    else
-    {
-      DcmElement* element = NULL;
-      if (!dataset.findAndGetElement(k, element).good() ||
-          element == NULL)
-      {
-        return false;
-      }
-
-      std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement
-                                  (*element, DicomToJsonFlags_Default, GetEncoding()));
-      
-      if (v.get() == NULL ||
-          v->IsNull())
-      {
-        value = "";
-      }
-      else
-      {
-        // TODO v->IsBinary()
-        value = v->GetContent();
-      }
-      
-      return true;
-    }
-  }
-
-
-  DicomInstanceHasher ParsedDicomFile::GetHasher()
-  {
-    std::string patientId, studyUid, seriesUid, instanceUid;
-
-    if (!GetTagValue(patientId, DICOM_TAG_PATIENT_ID) ||
-        !GetTagValue(studyUid, DICOM_TAG_STUDY_INSTANCE_UID) ||
-        !GetTagValue(seriesUid, DICOM_TAG_SERIES_INSTANCE_UID) ||
-        !GetTagValue(instanceUid, DICOM_TAG_SOP_INSTANCE_UID))
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    return DicomInstanceHasher(patientId, studyUid, seriesUid, instanceUid);
-  }
-
-
-  template <typename T>
-  static void ExtractPngImageTruncate(std::string& result,
-                                      DicomIntegerPixelAccessor& accessor,
-                                      PixelFormat format)
-  {
-    assert(accessor.GetInformation().GetChannelCount() == 1);
-
-    PngWriter w;
-
-    std::vector<T> image(accessor.GetInformation().GetWidth() * accessor.GetInformation().GetHeight(), 0);
-    T* pixel = &image[0];
-    for (unsigned int y = 0; y < accessor.GetInformation().GetHeight(); y++)
-    {
-      for (unsigned int x = 0; x < accessor.GetInformation().GetWidth(); x++, pixel++)
-      {
-        int32_t v = accessor.GetValue(x, y);
-        if (v < static_cast<int32_t>(std::numeric_limits<T>::min()))
-          *pixel = std::numeric_limits<T>::min();
-        else if (v > static_cast<int32_t>(std::numeric_limits<T>::max()))
-          *pixel = std::numeric_limits<T>::max();
-        else
-          *pixel = static_cast<T>(v);
-      }
-    }
-
-    w.WriteToMemory(result, accessor.GetInformation().GetWidth(), accessor.GetInformation().GetHeight(),
-                    accessor.GetInformation().GetWidth() * sizeof(T), format, &image[0]);
-  }
-
-
-  void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer)
-  {
-    FromDcmtkBridge::SaveToMemoryBuffer(buffer, *pimpl_->file_->getDataset());
-  }
-
-
-  void ParsedDicomFile::SaveToFile(const std::string& path)
-  {
-    // TODO Avoid using a temporary memory buffer, write directly on disk
-    std::string content;
-    SaveToMemoryBuffer(content);
-    Toolbox::WriteFile(content, path);
-  }
-
-
-  ParsedDicomFile::ParsedDicomFile() : pimpl_(new PImpl)
-  {
-    pimpl_->file_.reset(new DcmFileFormat);
-    Replace(DICOM_TAG_PATIENT_ID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient));
-    Replace(DICOM_TAG_STUDY_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study));
-    Replace(DICOM_TAG_SERIES_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series));
-    Replace(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance));
-  }
-
-
-  ParsedDicomFile::ParsedDicomFile(const char* content, size_t size) : pimpl_(new PImpl)
-  {
-    Setup(content, size);
-  }
-
-  ParsedDicomFile::ParsedDicomFile(const std::string& content) : pimpl_(new PImpl)
-  {
-    if (content.size() == 0)
-    {
-      Setup(NULL, 0);
-    }
-    else
-    {
-      Setup(&content[0], content.size());
-    }
-  }
-
-
-  ParsedDicomFile::ParsedDicomFile(ParsedDicomFile& other) : 
-    pimpl_(new PImpl)
-  {
-    pimpl_->file_.reset(dynamic_cast<DcmFileFormat*>(other.pimpl_->file_->clone()));
-
-    // Create a new instance-level identifier
-    Replace(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance));
-  }
-
-
-  ParsedDicomFile::~ParsedDicomFile()
-  {
-    delete pimpl_;
-  }
-
-
-  void* ParsedDicomFile::GetDcmtkObject()
-  {
-    return pimpl_->file_.get();
-  }
-
-
-  ParsedDicomFile* ParsedDicomFile::Clone()
-  {
-    return new ParsedDicomFile(*this);
-  }
-
-
-  void ParsedDicomFile::EmbedContent(const std::string& dataUriScheme)
-  {
-    std::string mime, content;
-    Toolbox::DecodeDataUriScheme(mime, content, dataUriScheme);
-    Toolbox::ToLowerCase(mime);
-
-    if (mime == "image/png")
-    {
-      EmbedImage(mime, content);
-    }
-    else if (mime == "application/pdf")
-    {
-      EmbedPdf(content);
-    }
-    else
-    {
-      LOG(ERROR) << "Unsupported MIME type for the content of a new DICOM file: " << mime;
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void ParsedDicomFile::EmbedImage(const std::string& mime,
-                                   const std::string& content)
-  {
-    if (mime == "image/png")
-    {
-      PngReader reader;
-      reader.ReadFromMemory(content);
-      EmbedImage(reader);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void ParsedDicomFile::EmbedImage(const ImageAccessor& accessor)
-  {
-    if (accessor.GetFormat() != PixelFormat_Grayscale8 &&
-        accessor.GetFormat() != PixelFormat_Grayscale16 &&
-        accessor.GetFormat() != PixelFormat_RGB24 &&
-        accessor.GetFormat() != PixelFormat_RGBA32)
-    {
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-
-    if (accessor.GetFormat() == PixelFormat_RGBA32)
-    {
-      LOG(WARNING) << "Getting rid of the alpha channel when embedding a RGBA image inside DICOM";
-    }
-
-    // http://dicomiseasy.blogspot.be/2012/08/chapter-12-pixel-data.html
-
-    Remove(DICOM_TAG_PIXEL_DATA);
-    Replace(DICOM_TAG_COLUMNS, boost::lexical_cast<std::string>(accessor.GetWidth()));
-    Replace(DICOM_TAG_ROWS, boost::lexical_cast<std::string>(accessor.GetHeight()));
-    Replace(DICOM_TAG_SAMPLES_PER_PIXEL, "1");
-    Replace(DICOM_TAG_NUMBER_OF_FRAMES, "1");
-    Replace(DICOM_TAG_PIXEL_REPRESENTATION, "0");  // Unsigned pixels
-    Replace(DICOM_TAG_PLANAR_CONFIGURATION, "0");  // Color channels are interleaved
-    Replace(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
-    Replace(DICOM_TAG_BITS_ALLOCATED, "8");
-    Replace(DICOM_TAG_BITS_STORED, "8");
-    Replace(DICOM_TAG_HIGH_BIT, "7");
-
-    unsigned int bytesPerPixel = 1;
-
-    switch (accessor.GetFormat())
-    {
-      case PixelFormat_RGB24:
-      case PixelFormat_RGBA32:
-        Replace(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "RGB");
-        Replace(DICOM_TAG_SAMPLES_PER_PIXEL, "3");
-        bytesPerPixel = 3;
-        break;
-
-      case PixelFormat_Grayscale8:
-        break;
-
-      case PixelFormat_Grayscale16:
-        Replace(DICOM_TAG_BITS_ALLOCATED, "16");
-        Replace(DICOM_TAG_BITS_STORED, "16");
-        Replace(DICOM_TAG_HIGH_BIT, "15");
-        bytesPerPixel = 2;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-
-    DcmTag key(DICOM_TAG_PIXEL_DATA.GetGroup(), 
-               DICOM_TAG_PIXEL_DATA.GetElement());
-
-    std::auto_ptr<DcmPixelData> pixels(new DcmPixelData(key));
-
-    unsigned int pitch = accessor.GetWidth() * bytesPerPixel;
-    Uint8* target = NULL;
-    pixels->createUint8Array(accessor.GetHeight() * pitch, target);
-
-    for (unsigned int y = 0; y < accessor.GetHeight(); y++)
-    {
-      switch (accessor.GetFormat())
-      {
-        case PixelFormat_RGB24:
-        case PixelFormat_Grayscale8:
-        case PixelFormat_Grayscale16:
-        case PixelFormat_SignedGrayscale16:
-        {
-          memcpy(target, reinterpret_cast<const Uint8*>(accessor.GetConstRow(y)), pitch);
-          target += pitch;
-          break;
-        }
-
-        case PixelFormat_RGBA32:
-        {
-          // The alpha channel is not supported by the DICOM standard
-          const Uint8* source = reinterpret_cast<const Uint8*>(accessor.GetConstRow(y));
-          for (unsigned int x = 0; x < accessor.GetWidth(); x++, target += 3, source += 4)
-          {
-            target[0] = source[0];
-            target[1] = source[1];
-            target[2] = source[2];
-          }
-
-          break;
-        }
-          
-        default:
-          throw OrthancException(ErrorCode_NotImplemented);
-      }
-    }
-
-    if (!pimpl_->file_->getDataset()->insert(pixels.release(), false, false).good())
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }    
-  }
-
-  
-  void ParsedDicomFile::ExtractImage(ImageBuffer& result,
-                                     unsigned int frame)
-  {
-    DcmDataset& dataset = *pimpl_->file_->getDataset();
-
-    if (!DicomImageDecoder::Decode(result, dataset, frame))
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-  }
-
-
-  void ParsedDicomFile::ExtractImage(ImageBuffer& result,
-                                     unsigned int frame,
-                                     ImageExtractionMode mode)
-  {
-    DcmDataset& dataset = *pimpl_->file_->getDataset();
-
-    bool ok = false;
-
-    switch (mode)
-    {
-      case ImageExtractionMode_UInt8:
-        ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_Grayscale8, false);
-        break;
-
-      case ImageExtractionMode_UInt16:
-        ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_Grayscale16, false);
-        break;
-
-      case ImageExtractionMode_Int16:
-        ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_SignedGrayscale16, false);
-        break;
-
-      case ImageExtractionMode_Preview:
-        ok = DicomImageDecoder::DecodePreview(result, dataset, frame);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    if (!ok)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-  }
-
-
-  void ParsedDicomFile::ExtractPngImage(std::string& result,
-                                        unsigned int frame,
-                                        ImageExtractionMode mode)
-  {
-    ImageBuffer buffer;
-    ExtractImage(buffer, frame, mode);
-
-    ImageAccessor accessor(buffer.GetConstAccessor());
-    PngWriter writer;
-    writer.WriteToMemory(result, accessor);
-  }
-
-
-  Encoding ParsedDicomFile::GetEncoding() const
-  {
-    return FromDcmtkBridge::DetectEncoding(*pimpl_->file_->getDataset());
-  }
-
-
-  void ParsedDicomFile::SetEncoding(Encoding encoding)
-  {
-    if (encoding == Encoding_Windows1251)
-    {
-      // This Cyrillic codepage is not officially supported by the
-      // DICOM standard. Do not set the SpecificCharacterSet tag.
-      return;
-    }
-
-    std::string s = GetDicomSpecificCharacterSet(encoding);
-    Replace(DICOM_TAG_SPECIFIC_CHARACTER_SET, s, DicomReplaceMode_InsertIfAbsent);
-  }
-
-  void ParsedDicomFile::ToJson(Json::Value& target, 
-                               DicomToJsonFormat format,
-                               DicomToJsonFlags flags,
-                               unsigned int maxStringLength)
-  {
-    FromDcmtkBridge::ToJson(target, *pimpl_->file_->getDataset(), format, flags, maxStringLength);
-  }
-
-
-  bool ParsedDicomFile::HasTag(const DicomTag& tag) const
-  {
-    DcmTag key(tag.GetGroup(), tag.GetElement());
-    return pimpl_->file_->getDataset()->tagExists(key);
-  }
-
-
-  void ParsedDicomFile::EmbedPdf(const std::string& pdf)
-  {
-    if (pdf.size() < 5 ||  // (*)
-        strncmp("%PDF-", pdf.c_str(), 5) != 0)
-    {
-      LOG(ERROR) << "Not a PDF file";
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    Replace(DICOM_TAG_SOP_CLASS_UID, UID_EncapsulatedPDFStorage);
-    Replace(FromDcmtkBridge::Convert(DCM_Modality), "OT");
-    Replace(FromDcmtkBridge::Convert(DCM_ConversionType), "WSD");
-    Replace(FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument), "application/pdf");
-    //Replace(FromDcmtkBridge::Convert(DCM_SeriesNumber), "1");
-
-    std::auto_ptr<DcmPolymorphOBOW> element(new DcmPolymorphOBOW(DCM_EncapsulatedDocument));
-
-    size_t s = pdf.size();
-    if (s & 1)
-    {
-      // The size of the buffer must be even
-      s += 1;
-    }
-
-    Uint8* bytes = NULL;
-    OFCondition result = element->createUint8Array(s, bytes);
-    if (!result.good() || bytes == NULL)
-    {
-      throw OrthancException(ErrorCode_NotEnoughMemory);
-    }
-
-    // Blank pad byte (no access violation, as "pdf.size() >= 5" because of (*) )
-    bytes[s - 1] = 0;
-
-    memcpy(bytes, pdf.c_str(), pdf.size());
-      
-    DcmPolymorphOBOW* obj = element.release();
-    result = pimpl_->file_->getDataset()->insert(obj);
-
-    if (!result.good())
-    {
-      delete obj;
-      throw OrthancException(ErrorCode_NotEnoughMemory);
-    }
-  }
-
-
-  bool ParsedDicomFile::ExtractPdf(std::string& pdf)
-  {
-    std::string sop, mime;
-    
-    if (!GetTagValue(sop, DICOM_TAG_SOP_CLASS_UID) ||
-        !GetTagValue(mime, FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument)) ||
-        sop != UID_EncapsulatedPDFStorage ||
-        mime != "application/pdf")
-    {
-      return false;
-    }
-
-    if (!GetTagValue(pdf, DICOM_TAG_ENCAPSULATED_DOCUMENT))
-    {
-      return false;
-    }
-
-    // Strip the possible pad byte at the end of file, because the
-    // encapsulated documents must always have an even length. The PDF
-    // format expects files to end with %%EOF followed by CR/LF. If
-    // the last character of the file is not a CR or LF, we assume it
-    // is a pad byte and remove it.
-    if (pdf.size() > 0)
-    {
-      char last = *pdf.rbegin();
-
-      if (last != 10 && last != 13)
-      {
-        pdf.resize(pdf.size() - 1);
-      }
-    }
-
-    return true;
-  }
-
-
-  void ParsedDicomFile::Convert(DicomMap& tags)
-  {
-    FromDcmtkBridge::Convert(tags, *pimpl_->file_->getDataset());
-  }
-}
--- a/OrthancServer/ParsedDicomFile.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,150 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../Core/DicomFormat/DicomInstanceHasher.h"
-#include "../Core/RestApi/RestApiOutput.h"
-#include "ServerEnumerations.h"
-#include "../Core/Images/ImageAccessor.h"
-#include "../Core/Images/ImageBuffer.h"
-#include "../Core/IDynamicObject.h"
-
-namespace Orthanc
-{
-  class ParsedDicomFile : public IDynamicObject
-  {
-  private:
-    struct PImpl;
-    PImpl* pimpl_;
-
-    ParsedDicomFile(ParsedDicomFile& other);
-
-    void Setup(const char* content,
-               size_t size);
-
-    void RemovePrivateTagsInternal(const std::set<DicomTag>* toKeep);
-
-    void UpdateStorageUid(const DicomTag& tag,
-                          const std::string& value,
-                          bool decodeBinaryTags);
-
-  public:
-    ParsedDicomFile();  // Create a minimal DICOM instance
-
-    ParsedDicomFile(const char* content,
-                    size_t size);
-
-    ParsedDicomFile(const std::string& content);
-
-    ~ParsedDicomFile();
-
-    void* GetDcmtkObject();
-
-    ParsedDicomFile* Clone();
-
-    void SendPathValue(RestApiOutput& output,
-                       const UriComponents& uri);
-
-    void Answer(RestApiOutput& output);
-
-    void Remove(const DicomTag& tag);
-
-    void Replace(const DicomTag& tag,
-                 const std::string& utf8Value,
-                 DicomReplaceMode mode = DicomReplaceMode_InsertIfAbsent);
-
-    void Replace(const DicomTag& tag,
-                 const Json::Value& value,  // Assumed to be encoded with UTF-8
-                 bool decodeBinaryTags,
-                 DicomReplaceMode mode = DicomReplaceMode_InsertIfAbsent);
-
-    void Insert(const DicomTag& tag,
-                const Json::Value& value,   // Assumed to be encoded with UTF-8
-                bool decodeBinaryTags);
-
-    void RemovePrivateTags()
-    {
-      RemovePrivateTagsInternal(NULL);
-    }
-
-    void RemovePrivateTags(const std::set<DicomTag>& toKeep)
-    {
-      RemovePrivateTagsInternal(&toKeep);
-    }
-
-    bool GetTagValue(std::string& value,
-                     const DicomTag& tag);
-
-    DicomInstanceHasher GetHasher();
-
-    void SaveToMemoryBuffer(std::string& buffer);
-
-    void SaveToFile(const std::string& path);
-
-    void EmbedContent(const std::string& dataUriScheme);
-
-    void EmbedImage(const ImageAccessor& accessor);
-
-    void EmbedImage(const std::string& mime,
-                    const std::string& content);
-
-    void ExtractImage(ImageBuffer& result,
-                      unsigned int frame);
-
-    void ExtractImage(ImageBuffer& result,
-                      unsigned int frame,
-                      ImageExtractionMode mode);
-
-    void ExtractPngImage(std::string& result,
-                         unsigned int frame,
-                         ImageExtractionMode mode);
-
-    Encoding GetEncoding() const;
-
-    void SetEncoding(Encoding encoding);
-
-    void ToJson(Json::Value& target, 
-                DicomToJsonFormat format,
-                DicomToJsonFlags flags,
-                unsigned int maxStringLength);
-
-    bool HasTag(const DicomTag& tag) const;
-
-    void EmbedPdf(const std::string& pdf);
-
-    bool ExtractPdf(std::string& pdf);
-
-    void Convert(DicomMap& tags);
-  };
-
-}
--- a/OrthancServer/PrecompiledHeadersServer.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/PrecompiledHeadersServer.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/PrecompiledHeadersServer.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/PrecompiledHeadersServer.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -36,42 +37,6 @@
 
 #if ORTHANC_USE_PRECOMPILED_HEADERS == 1
 
-// DCMTK
-#include <dcmtk/dcmdata/dcchrstr.h>
-#include <dcmtk/dcmdata/dcdeftag.h>
-#include <dcmtk/dcmdata/dcdicent.h>
-#include <dcmtk/dcmdata/dcdict.h>
-#include <dcmtk/dcmdata/dcfilefo.h>
-#include <dcmtk/dcmdata/dcistrmb.h>
-#include <dcmtk/dcmdata/dcistrmf.h>
-#include <dcmtk/dcmdata/dcmetinf.h>
-#include <dcmtk/dcmdata/dcostrmb.h>
-#include <dcmtk/dcmdata/dcpixel.h>
-#include <dcmtk/dcmdata/dcpixseq.h>
-#include <dcmtk/dcmdata/dcpxitem.h>
-#include <dcmtk/dcmdata/dcuid.h>
-#include <dcmtk/dcmdata/dcvrae.h>
-#include <dcmtk/dcmdata/dcvras.h>
-#include <dcmtk/dcmdata/dcvrcs.h>
-#include <dcmtk/dcmdata/dcvrda.h>
-#include <dcmtk/dcmdata/dcvrds.h>
-#include <dcmtk/dcmdata/dcvrdt.h>
-#include <dcmtk/dcmdata/dcvrfd.h>
-#include <dcmtk/dcmdata/dcvrfl.h>
-#include <dcmtk/dcmdata/dcvris.h>
-#include <dcmtk/dcmdata/dcvrlo.h>
-#include <dcmtk/dcmdata/dcvrlt.h>
-#include <dcmtk/dcmdata/dcvrpn.h>
-#include <dcmtk/dcmdata/dcvrsh.h>
-#include <dcmtk/dcmdata/dcvrsl.h>
-#include <dcmtk/dcmdata/dcvrss.h>
-#include <dcmtk/dcmdata/dcvrst.h>
-#include <dcmtk/dcmdata/dcvrtm.h>
-#include <dcmtk/dcmdata/dcvrui.h>
-#include <dcmtk/dcmdata/dcvrul.h>
-#include <dcmtk/dcmdata/dcvrus.h>
-#include <dcmtk/dcmdata/dcvrut.h>
-#include <dcmtk/dcmnet/dcasccfg.h>
-#include <dcmtk/dcmnet/diutil.h>
+#include "ServerContext.h"
 
 #endif
--- a/OrthancServer/QueryRetrieveHandler.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/QueryRetrieveHandler.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -35,9 +36,45 @@
 
 #include "OrthancInitialization.h"
 
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/Logging.h"
+
 
 namespace Orthanc
 {
+  static void FixQueryLua(DicomMap& query,
+                          ServerContext& context,
+                          const std::string& modality)
+  {
+    static const char* LUA_CALLBACK = "OutgoingFindRequestFilter";
+
+    LuaScripting::Lock lock(context.GetLuaScripting());
+
+    if (lock.GetLua().IsExistingFunction(LUA_CALLBACK))
+    {
+      LuaFunctionCall call(lock.GetLua(), LUA_CALLBACK);
+      call.PushDicom(query);
+      call.PushJson(modality);
+      FromDcmtkBridge::ExecuteToDicom(query, call);
+    }
+  }
+
+
+  static void FixQueryBuiltin(DicomMap& query,
+                              ModalityManufacturer manufacturer)
+  {
+    /**
+     * Introduce patches for specific manufacturers below.
+     **/
+
+    switch (manufacturer)
+    {
+      default:
+        break;
+    }
+  }
+
+
   void QueryRetrieveHandler::Invalidate()
   {
     done_ = false;
@@ -49,8 +86,20 @@
   {
     if (!done_)
     {
-      ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_);
-      locker.GetConnection().Find(answers_, level_, query_);
+      // Firstly, fix the content of the query for specific manufacturers
+      DicomMap fixed;
+      fixed.Assign(query_);
+      FixQueryBuiltin(fixed, modality_.GetManufacturer());
+
+      // Secondly, possibly fix the query with the user-provider Lua callback
+      FixQueryLua(fixed, context_, modality_.GetApplicationEntityTitle()); 
+
+      {
+        DicomUserConnection connection(localAet_, modality_);
+        connection.Open();
+        connection.Find(answers_, level_, fixed);
+      }
+
       done_ = true;
     }
   }
@@ -60,7 +109,8 @@
     context_(context),
     localAet_(context.GetDefaultLocalApplicationEntityTitle()),
     done_(false),
-    level_(ResourceType_Study)
+    level_(ResourceType_Study),
+    answers_(false)
   {
   }
 
@@ -84,50 +134,21 @@
                                       const std::string& value)
   {
     Invalidate();
-    query_.SetValue(tag, value);
+    query_.SetValue(tag, value, false);
   }
 
 
-  size_t QueryRetrieveHandler::GetAnswerCount()
+  size_t QueryRetrieveHandler::GetAnswersCount()
   {
     Run();
     return answers_.GetSize();
   }
 
 
-  const DicomMap& QueryRetrieveHandler::GetAnswer(size_t i)
-  {
-    Run();
-
-    if (i >= answers_.GetSize())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    return answers_.GetAnswer(i);
-  }
-
-
-  void QueryRetrieveHandler::Retrieve(const std::string& target,
-                                      size_t i)
+  void QueryRetrieveHandler::GetAnswer(DicomMap& target,
+                                       size_t i)
   {
     Run();
-
-    if (i >= answers_.GetSize())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_);
-    locker.GetConnection().Move(target, answers_.GetAnswer(i));
-  }
-
-
-  void QueryRetrieveHandler::Retrieve(const std::string& target)
-  {
-    for (size_t i = 0; i < GetAnswerCount(); i++)
-    {
-      Retrieve(target, i);
-    }
+    answers_.GetAnswer(i).ExtractDicomSummary(target);
   }
 }
--- a/OrthancServer/QueryRetrieveHandler.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/QueryRetrieveHandler.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -40,7 +41,7 @@
   {
   private:
     ServerContext&             context_;
-    const std::string&         localAet_;
+    std::string                localAet_;
     bool                       done_;
     RemoteModalityParameters   modality_;
     ResourceType               level_;
@@ -50,17 +51,21 @@
 
     void Invalidate();
 
-
   public:
     QueryRetrieveHandler(ServerContext& context);
 
     void SetModality(const std::string& symbolicName);
 
-    const RemoteModalityParameters& GetModality() const
+    const RemoteModalityParameters& GetRemoteModality() const
     {
       return modality_;
     }
 
+    const std::string& GetLocalAet() const
+    {
+      return localAet_;
+    }
+
     const std::string& GetModalitySymbolicName() const
     {
       return modalityName_;
@@ -83,13 +88,9 @@
 
     void Run();
 
-    size_t GetAnswerCount();
-
-    const DicomMap& GetAnswer(size_t i);
+    size_t GetAnswersCount();
 
-    void Retrieve(const std::string& target,
-                  size_t i);
-
-    void Retrieve(const std::string& target);
+    void GetAnswer(DicomMap& target,
+                   size_t i);
   };
 }
--- a/OrthancServer/Scheduler/CallSystemCommand.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "CallSystemCommand.h"
-
-#include "../../Core/Logging.h"
-#include "../../Core/Toolbox.h"
-#include "../../Core/Uuid.h"
-
-namespace Orthanc
-{
-  CallSystemCommand::CallSystemCommand(ServerContext& context,
-                                       const std::string& command,
-                                       const std::vector<std::string>& arguments) : 
-    context_(context),
-    command_(command),
-    arguments_(arguments)
-  {
-  }
-
-  bool CallSystemCommand::Apply(ListOfStrings& outputs,
-                                const ListOfStrings& inputs)
-  {
-    for (ListOfStrings::const_iterator
-           it = inputs.begin(); it != inputs.end(); ++it)
-    {
-      LOG(INFO) << "Calling system command " << command_ << " on instance " << *it;
-
-      try
-      {
-        std::string dicom;
-        context_.ReadFile(dicom, *it, FileContentType_Dicom);
-
-        Toolbox::TemporaryFile tmp;
-        tmp.Write(dicom);
-
-        std::vector<std::string> args = arguments_;
-        args.push_back(tmp.GetPath());
-
-        Toolbox::ExecuteSystemCommand(command_, args);
-
-        // Only chain with other commands if this command succeeds
-        outputs.push_back(*it);
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Unable to call system command " << command_ 
-                   << " on instance " << *it << " in a Lua script: " << e.What();
-      }
-    }
-
-    return true;
-  }
-}
--- a/OrthancServer/Scheduler/CallSystemCommand.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IServerCommand.h"
-#include "../ServerContext.h"
-
-namespace Orthanc
-{
-  class CallSystemCommand : public IServerCommand
-  {
-  private:
-    ServerContext& context_;
-    std::string command_;
-    std::vector<std::string> arguments_;
-
-  public:
-    CallSystemCommand(ServerContext& context,
-                      const std::string& command,
-                      const std::vector<std::string>& arguments);
-
-    virtual bool Apply(ListOfStrings& outputs,
-                       const ListOfStrings& inputs);
-  };
-}
--- a/OrthancServer/Scheduler/DeleteInstanceCommand.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "DeleteInstanceCommand.h"
-
-#include "../../Core/Logging.h"
-
-namespace Orthanc
-{
-  bool DeleteInstanceCommand::Apply(ListOfStrings& outputs,
-                                    const ListOfStrings& inputs)
-  {
-    for (ListOfStrings::const_iterator
-           it = inputs.begin(); it != inputs.end(); ++it)
-    {
-      LOG(INFO) << "Deleting instance " << *it;
-
-      try
-      {
-        Json::Value tmp;
-        context_.DeleteResource(tmp, *it, ResourceType_Instance);
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Unable to delete instance " << *it << " in a Lua script: " << e.What();
-      }
-    }
-
-    return true;
-  }
-}
--- a/OrthancServer/Scheduler/DeleteInstanceCommand.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IServerCommand.h"
-#include "../ServerContext.h"
-
-namespace Orthanc
-{
-  class DeleteInstanceCommand : public IServerCommand
-  {
-  private:
-    ServerContext& context_;
-
-  public:
-    DeleteInstanceCommand(ServerContext& context) : context_(context)
-    {
-    }
-
-    virtual bool Apply(ListOfStrings& outputs,
-                       const ListOfStrings& inputs);
-  };
-}
--- a/OrthancServer/Scheduler/IServerCommand.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include <list>
-#include <string>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class IServerCommand : public boost::noncopyable
-  {
-  public:
-    typedef std::list<std::string>  ListOfStrings;
-
-    virtual ~IServerCommand()
-    {
-    }
-
-    virtual bool Apply(ListOfStrings& outputs,
-                       const ListOfStrings& inputs) = 0;
-  };
-}
--- a/OrthancServer/Scheduler/ModifyInstanceCommand.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,123 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "ModifyInstanceCommand.h"
-
-#include "../../Core/Logging.h"
-
-namespace Orthanc
-{
-  ModifyInstanceCommand::ModifyInstanceCommand(ServerContext& context,
-                                               RequestOrigin origin,
-                                               DicomModification* modification) :
-    context_(context),
-    origin_(origin),
-    modification_(modification)
-  {
-    modification_->SetAllowManualIdentifiers(true);
-
-    if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID))
-    {
-      modification_->SetLevel(ResourceType_Patient);
-    }
-    else if (modification_->IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
-    {
-      modification_->SetLevel(ResourceType_Study);
-    }
-    else if (modification_->IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
-    {
-      modification_->SetLevel(ResourceType_Series);
-    }
-    else
-    {
-      modification_->SetLevel(ResourceType_Instance);
-    }
-
-    if (origin_ != RequestOrigin_Lua)
-    {
-      // TODO If issued from HTTP, "remoteIp" and "username" must be provided
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  ModifyInstanceCommand::~ModifyInstanceCommand()
-  {
-    if (modification_)
-    {
-      delete modification_;
-    }
-  }
-
-
-  bool ModifyInstanceCommand::Apply(ListOfStrings& outputs,
-                                    const ListOfStrings& inputs)
-  {
-    for (ListOfStrings::const_iterator
-           it = inputs.begin(); it != inputs.end(); ++it)
-    {
-      LOG(INFO) << "Modifying resource " << *it;
-
-      try
-      {
-        std::auto_ptr<ParsedDicomFile> modified;
-
-        {
-          ServerContext::DicomCacheLocker lock(context_, *it);
-          modified.reset(lock.GetDicom().Clone());
-        }
-
-        modification_->Apply(*modified);
-
-        DicomInstanceToStore toStore;
-        assert(origin_ == RequestOrigin_Lua);
-        toStore.SetLuaOrigin();
-        toStore.SetParsedDicomFile(*modified);
-        // TODO other metadata
-        toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, *it);
-
-        std::string modifiedId;
-        context_.Store(modifiedId, toStore);
-
-        // Only chain with other commands if this command succeeds
-        outputs.push_back(modifiedId);
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Unable to modify instance " << *it << " in a Lua script: " << e.What();
-      }
-    }
-
-    return true;
-  }
-}
--- a/OrthancServer/Scheduler/ModifyInstanceCommand.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IServerCommand.h"
-#include "../ServerContext.h"
-#include "../DicomModification.h"
-
-namespace Orthanc
-{
-  class ModifyInstanceCommand : public IServerCommand
-  {
-  private:
-    ServerContext& context_;
-    RequestOrigin origin_;
-    DicomModification* modification_;
-
-  public:
-    ModifyInstanceCommand(ServerContext& context,
-                          RequestOrigin origin,
-                          DicomModification* modification);  // takes the ownership
-
-    virtual ~ModifyInstanceCommand();
-
-    const DicomModification& GetModification() const
-    {
-      return *modification_;
-    }
-
-    virtual bool Apply(ListOfStrings& outputs,
-                       const ListOfStrings& inputs);
-  };
-}
--- a/OrthancServer/Scheduler/ServerCommandInstance.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,98 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "ServerCommandInstance.h"
-
-#include "../../Core/OrthancException.h"
-
-namespace Orthanc
-{
-  bool ServerCommandInstance::Execute(IListener& listener)
-  {
-    ListOfStrings outputs;
-
-    bool success = false;
-
-    try
-    {
-      if (command_->Apply(outputs, inputs_))
-      {
-        success = true;
-      }
-    }
-    catch (OrthancException&)
-    {
-    }
-
-    if (!success)
-    {
-      listener.SignalFailure(jobId_);
-      return true;
-    }
-
-    for (std::list<ServerCommandInstance*>::iterator
-           it = next_.begin(); it != next_.end(); ++it)
-    {
-      for (ListOfStrings::const_iterator
-             output = outputs.begin(); output != outputs.end(); ++output)
-      {
-        (*it)->AddInput(*output);
-      }
-    }
-
-    listener.SignalSuccess(jobId_);
-    return true;
-  }
-
-
-  ServerCommandInstance::ServerCommandInstance(IServerCommand *command,
-                                               const std::string& jobId) : 
-    command_(command), 
-    jobId_(jobId),
-    connectedToSink_(false)
-  {
-    if (command_ == NULL)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  ServerCommandInstance::~ServerCommandInstance()
-  {
-    if (command_ != NULL)
-    {
-      delete command_;
-    }
-  }
-}
--- a/OrthancServer/Scheduler/ServerCommandInstance.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,104 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../../Core/IDynamicObject.h"
-#include "IServerCommand.h"
-
-namespace Orthanc
-{
-  class ServerCommandInstance : public IDynamicObject
-  {
-    friend class ServerScheduler;
-
-  public:
-    class IListener
-    {
-    public:
-      virtual ~IListener()
-      {
-      }
-
-      virtual void SignalSuccess(const std::string& jobId) = 0;
-
-      virtual void SignalFailure(const std::string& jobId) = 0;
-    };
-
-  private:
-    typedef IServerCommand::ListOfStrings  ListOfStrings;
-
-    IServerCommand *command_;
-    std::string jobId_;
-    ListOfStrings inputs_;
-    std::list<ServerCommandInstance*> next_;
-    bool connectedToSink_;
-
-    bool Execute(IListener& listener);
-
-  public:
-    ServerCommandInstance(IServerCommand *command,
-                          const std::string& jobId);
-
-    virtual ~ServerCommandInstance();
-
-    const std::string& GetJobId() const
-    {
-      return jobId_;
-    }
-
-    void AddInput(const std::string& input)
-    {
-      inputs_.push_back(input);
-    }
-
-    void ConnectOutput(ServerCommandInstance& next)
-    {
-      next_.push_back(&next);
-    }
-
-    void SetConnectedToSink(bool connected = true)
-    {
-      connectedToSink_ = connected;
-    }
-
-    bool IsConnectedToSink() const
-    {
-      return connectedToSink_;
-    }
-
-    const std::list<ServerCommandInstance*>& GetNextCommands() const
-    {
-      return next_;
-    }
-  };
-}
--- a/OrthancServer/Scheduler/ServerJob.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,147 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "ServerJob.h"
-
-#include "../../Core/OrthancException.h"
-#include "../../Core/Toolbox.h"
-#include "../../Core/Uuid.h"
-
-namespace Orthanc
-{
-  void ServerJob::CheckOrdering()
-  {
-    std::map<ServerCommandInstance*, unsigned int> index;
-
-    unsigned int count = 0;
-    for (std::list<ServerCommandInstance*>::const_iterator
-           it = filters_.begin(); it != filters_.end(); ++it)
-    {
-      index[*it] = count++;
-    }
-
-    for (std::list<ServerCommandInstance*>::const_iterator
-           it = filters_.begin(); it != filters_.end(); ++it)
-    {
-      const std::list<ServerCommandInstance*>& nextCommands = (*it)->GetNextCommands();
-
-      for (std::list<ServerCommandInstance*>::const_iterator
-             next = nextCommands.begin(); next != nextCommands.end(); ++next)
-      {
-        if (index.find(*next) == index.end() ||
-            index[*next] <= index[*it])
-        {
-          // You must reorder your calls to "ServerJob::AddCommand"
-          throw OrthancException(ErrorCode_BadJobOrdering);
-        }
-      }
-    }
-  }
-
-
-  size_t ServerJob::Submit(SharedMessageQueue& target,
-                           ServerCommandInstance::IListener& listener)
-  {
-    if (submitted_)
-    {
-      // This job has already been submitted
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    CheckOrdering();
-
-    size_t size = filters_.size();
-
-    for (std::list<ServerCommandInstance*>::iterator 
-           it = filters_.begin(); it != filters_.end(); ++it)
-    {
-      target.Enqueue(*it);
-    }
-
-    filters_.clear();
-    submitted_ = true;
-
-    return size;
-  }
-
-
-  ServerJob::ServerJob() :
-    jobId_(Toolbox::GenerateUuid()),
-    submitted_(false),
-    description_("no description")
-  {
-  }
-
-
-  ServerJob::~ServerJob()
-  {
-    for (std::list<ServerCommandInstance*>::iterator
-           it = filters_.begin(); it != filters_.end(); ++it)
-    {
-      delete *it;
-    }
-
-    for (std::list<IDynamicObject*>::iterator
-           it = payloads_.begin(); it != payloads_.end(); ++it)
-    {
-      delete *it;
-    }
-  }
-
-
-  ServerCommandInstance& ServerJob::AddCommand(IServerCommand* filter)
-  {
-    if (submitted_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    filters_.push_back(new ServerCommandInstance(filter, jobId_));
-      
-    return *filters_.back();
-  }
-
-
-  IDynamicObject& ServerJob::AddPayload(IDynamicObject* payload)
-  {
-    if (submitted_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    payloads_.push_back(payload);
-      
-    return *filters_.back();
-  }
-
-}
--- a/OrthancServer/Scheduler/ServerJob.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ServerCommandInstance.h"
-#include "../../Core/MultiThreading/SharedMessageQueue.h"
-
-namespace Orthanc
-{
-  class ServerJob
-  {
-    friend class ServerScheduler;
-
-  private:
-    std::list<ServerCommandInstance*> filters_;
-    std::list<IDynamicObject*> payloads_;
-    std::string jobId_;
-    bool submitted_;
-    std::string description_;
-
-    void CheckOrdering();
-
-    size_t Submit(SharedMessageQueue& target,
-                  ServerCommandInstance::IListener& listener);
-
-  public:
-    ServerJob();
-
-    ~ServerJob();
-
-    const std::string& GetId() const
-    {
-      return jobId_;
-    }
-
-    void SetDescription(const std::string& description)
-    {
-      description_ = description;
-    }
-
-    const std::string& GetDescription() const
-    {
-      return description_;
-    }
-
-    ServerCommandInstance& AddCommand(IServerCommand* filter);
-
-    // Take the ownership of a payload to a job. This payload will be
-    // automatically freed when the job succeeds or fails.
-    IDynamicObject& AddPayload(IDynamicObject* payload);
-  };
-}
--- a/OrthancServer/Scheduler/ServerScheduler.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,353 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "ServerScheduler.h"
-
-#include "../../Core/OrthancException.h"
-#include "../../Core/Logging.h"
-
-namespace Orthanc
-{
-  namespace
-  {
-    // Anonymous namespace to avoid clashes between compilation modules
-    class Sink : public IServerCommand
-    {
-    private:
-      ListOfStrings& target_;
-
-    public:
-      Sink(ListOfStrings& target) : target_(target)
-      {
-      }
-
-      virtual bool Apply(ListOfStrings& outputs,
-                         const ListOfStrings& inputs)
-      {
-        for (ListOfStrings::const_iterator 
-               it = inputs.begin(); it != inputs.end(); ++it)
-        {
-          target_.push_back(*it);
-        }
-
-        return true;
-      }    
-    };
-  }
-
-
-  ServerScheduler::JobInfo& ServerScheduler::GetJobInfo(const std::string& jobId)
-  {
-    Jobs::iterator info = jobs_.find(jobId);
-
-    if (info == jobs_.end())
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    return info->second;
-  }
-
-
-  void ServerScheduler::SignalSuccess(const std::string& jobId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    JobInfo& info = GetJobInfo(jobId);
-    info.success_++;
-
-    assert(info.failures_ == 0);
-
-    if (info.success_ >= info.size_)
-    {
-      if (info.watched_)
-      {
-        watchedJobStatus_[jobId] = JobStatus_Success;
-        watchedJobFinished_.notify_all();
-      }
-
-      LOG(INFO) << "Job successfully finished (" << info.description_ << ")";
-      jobs_.erase(jobId);
-
-      availableJob_.Release();
-    }
-  }
-
-
-  void ServerScheduler::SignalFailure(const std::string& jobId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    JobInfo& info = GetJobInfo(jobId);
-    info.failures_++;
-
-    if (info.success_ + info.failures_ >= info.size_)
-    {
-      if (info.watched_)
-      {
-        watchedJobStatus_[jobId] = JobStatus_Failure;
-        watchedJobFinished_.notify_all();
-      }
-
-      LOG(ERROR) << "Job has failed (" << info.description_ << ")";
-      jobs_.erase(jobId);
-
-      availableJob_.Release();
-    }
-  }
-
-
-  void ServerScheduler::Worker(ServerScheduler* that)
-  {
-    static const int32_t TIMEOUT = 100;
-
-    LOG(WARNING) << "The server scheduler has started";
-
-    while (!that->finish_)
-    {
-      std::auto_ptr<IDynamicObject> object(that->queue_.Dequeue(TIMEOUT));
-      if (object.get() != NULL)
-      {
-        ServerCommandInstance& filter = dynamic_cast<ServerCommandInstance&>(*object);
-
-        // Skip the execution of this filter if its parent job has
-        // previously failed.
-        bool jobHasFailed;
-        {
-          boost::mutex::scoped_lock lock(that->mutex_);
-          JobInfo& info = that->GetJobInfo(filter.GetJobId());
-          jobHasFailed = (info.failures_ > 0 || info.cancel_); 
-        }
-
-        if (jobHasFailed)
-        {
-          that->SignalFailure(filter.GetJobId());
-        }
-        else
-        {
-          filter.Execute(*that);
-        }
-      }
-    }
-  }
-
-
-  void ServerScheduler::SubmitInternal(ServerJob& job,
-                                       bool watched)
-  {
-    availableJob_.Acquire();
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    JobInfo info;
-    info.size_ = job.Submit(queue_, *this);
-    info.cancel_ = false;
-    info.success_ = 0;
-    info.failures_ = 0;
-    info.description_ = job.GetDescription();
-    info.watched_ = watched;
-
-    assert(info.size_ > 0);
-
-    if (watched)
-    {
-      watchedJobStatus_[job.GetId()] = JobStatus_Running;
-    }
-
-    jobs_[job.GetId()] = info;
-
-    LOG(INFO) << "New job submitted (" << job.description_ << ")";
-  }
-
-
-  ServerScheduler::ServerScheduler(unsigned int maxJobs) : availableJob_(maxJobs)
-  {
-    finish_ = false;
-    worker_ = boost::thread(Worker, this);
-  }
-
-
-  ServerScheduler::~ServerScheduler()
-  {
-    if (!finish_)
-    {
-      LOG(ERROR) << "INTERNAL ERROR: ServerScheduler::Finalize() should be invoked manually to avoid mess in the destruction order!";
-      Stop();
-    }
-  }
-
-
-  void ServerScheduler::Stop()
-  {
-    if (!finish_)
-    {
-      finish_ = true;
-
-      if (worker_.joinable())
-      {
-        worker_.join();
-      }
-    }
-  }
-
-
-  void ServerScheduler::Submit(ServerJob& job)
-  {
-    if (job.filters_.empty())
-    {
-      return;
-    }
-
-    SubmitInternal(job, false);
-  }
-
-
-  bool ServerScheduler::SubmitAndWait(ListOfStrings& outputs,
-                                      ServerJob& job)
-  {
-    std::string jobId = job.GetId();
-
-    outputs.clear();
-
-    if (job.filters_.empty())
-    {
-      return true;
-    }
-
-    // Add a sink filter to collect all the results of the filters
-    // that have no next filter.
-    ServerCommandInstance& sink = job.AddCommand(new Sink(outputs));
-
-    for (std::list<ServerCommandInstance*>::iterator
-           it = job.filters_.begin(); it != job.filters_.end(); ++it)
-    {
-      if ((*it) != &sink &&
-          (*it)->IsConnectedToSink())
-      {
-        (*it)->ConnectOutput(sink);
-      }
-    }
-
-    // Submit the job
-    SubmitInternal(job, true);
-
-    // Wait for the job to complete (either success or failure)
-    JobStatus status;
-
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-
-      assert(watchedJobStatus_.find(jobId) != watchedJobStatus_.end());
-        
-      while (watchedJobStatus_[jobId] == JobStatus_Running)
-      {
-        watchedJobFinished_.wait(lock);
-      }
-
-      status = watchedJobStatus_[jobId];
-      watchedJobStatus_.erase(jobId);
-    }
-
-    return (status == JobStatus_Success);
-  }
-
-
-  bool ServerScheduler::SubmitAndWait(ServerJob& job)
-  {
-    ListOfStrings ignoredSink;
-    return SubmitAndWait(ignoredSink, job);
-  }
-
-
-  bool ServerScheduler::IsRunning(const std::string& jobId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    return jobs_.find(jobId) != jobs_.end();
-  }
-
-
-  void ServerScheduler::Cancel(const std::string& jobId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Jobs::iterator job = jobs_.find(jobId);
-
-    if (job != jobs_.end())
-    {
-      job->second.cancel_ = true;
-      LOG(WARNING) << "Canceling a job (" << job->second.description_ << ")";
-    }
-  }
-
-
-  float ServerScheduler::GetProgress(const std::string& jobId) 
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Jobs::iterator job = jobs_.find(jobId);
-
-    if (job == jobs_.end() || 
-        job->second.size_ == 0  /* should never happen */)
-    {
-      // This job is not running
-      return 1;
-    }
-
-    if (job->second.failures_ != 0)
-    {
-      return 1;
-    }
-
-    if (job->second.size_ == 1)
-    {
-      return static_cast<float>(job->second.success_);
-    }
-
-    return (static_cast<float>(job->second.success_) / 
-            static_cast<float>(job->second.size_ - 1));
-  }
-
-
-  void ServerScheduler::GetListOfJobs(ListOfStrings& jobs)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    jobs.clear();
-
-    for (Jobs::const_iterator 
-           it = jobs_.begin(); it != jobs_.end(); ++it)
-    {
-      jobs.push_back(it->first);
-    }
-  }
-}
--- a/OrthancServer/Scheduler/ServerScheduler.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ServerJob.h"
-
-#include "../../Core/MultiThreading/Semaphore.h"
-
-namespace Orthanc
-{
-  class ServerScheduler : public ServerCommandInstance::IListener
-  {
-  private:
-    struct JobInfo
-    {
-      bool watched_;
-      bool cancel_;
-      size_t size_;
-      size_t success_;
-      size_t failures_;
-      std::string description_;
-    };
-
-    enum JobStatus
-    {
-      JobStatus_Running = 1,
-      JobStatus_Success = 2,
-      JobStatus_Failure = 3
-    };
-
-    typedef IServerCommand::ListOfStrings  ListOfStrings;
-    typedef std::map<std::string, JobInfo> Jobs;
-
-    boost::mutex mutex_;
-    boost::condition_variable watchedJobFinished_;
-    Jobs jobs_;
-    SharedMessageQueue queue_;
-    bool finish_;
-    boost::thread worker_;
-    std::map<std::string, JobStatus> watchedJobStatus_;
-    Semaphore availableJob_;
-
-    JobInfo& GetJobInfo(const std::string& jobId);
-
-    virtual void SignalSuccess(const std::string& jobId);
-
-    virtual void SignalFailure(const std::string& jobId);
-
-    static void Worker(ServerScheduler* that);
-
-    void SubmitInternal(ServerJob& job,
-                        bool watched);
-
-  public:
-    ServerScheduler(unsigned int maxjobs);
-
-    ~ServerScheduler();
-
-    void Stop();
-
-    void Submit(ServerJob& job);
-
-    bool SubmitAndWait(ListOfStrings& outputs,
-                       ServerJob& job);
-
-    bool SubmitAndWait(ServerJob& job);
-
-    bool IsRunning(const std::string& jobId);
-
-    void Cancel(const std::string& jobId);
-
-    // Returns a number between 0 and 1
-    float GetProgress(const std::string& jobId);
-
-    bool IsRunning(const ServerJob& job)
-    {
-      return IsRunning(job.GetId());
-    }
-
-    void Cancel(const ServerJob& job) 
-    {
-      Cancel(job.GetId());
-    }
-
-    float GetProgress(const ServerJob& job) 
-    {
-      return GetProgress(job.GetId());
-    }
-
-    void GetListOfJobs(ListOfStrings& jobs);
-  };
-}
--- a/OrthancServer/Scheduler/StorePeerCommand.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "StorePeerCommand.h"
-
-#include "../../Core/Logging.h"
-#include "../../Core/HttpClient.h"
-
-namespace Orthanc
-{
-  StorePeerCommand::StorePeerCommand(ServerContext& context,
-                                     const OrthancPeerParameters& peer,
-                                     bool ignoreExceptions) : 
-    context_(context),
-    peer_(peer),
-    ignoreExceptions_(ignoreExceptions)
-  {
-  }
-
-  bool StorePeerCommand::Apply(ListOfStrings& outputs,
-                               const ListOfStrings& inputs)
-  {
-    // Configure the HTTP client
-    HttpClient client;
-    client.SetProxy(Configuration::GetGlobalStringParameter("HttpProxy", ""));
-    if (peer_.GetUsername().size() != 0 && 
-        peer_.GetPassword().size() != 0)
-    {
-      client.SetCredentials(peer_.GetUsername().c_str(), 
-                            peer_.GetPassword().c_str());
-    }
-
-    client.SetUrl(peer_.GetUrl() + "instances");
-    client.SetMethod(HttpMethod_Post);
-
-    for (ListOfStrings::const_iterator
-           it = inputs.begin(); it != inputs.end(); ++it)
-    {
-      LOG(INFO) << "Sending resource " << *it << " to peer \"" 
-                << peer_.GetUrl() << "\"";
-
-      try
-      {
-        context_.ReadFile(client.GetBody(), *it, FileContentType_Dicom);
-
-        std::string answer;
-        if (!client.Apply(answer))
-        {
-          LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << peer_.GetUrl() << "\"";
-          throw OrthancException(ErrorCode_NetworkProtocol);
-        }
-
-        // Only chain with other commands if this command succeeds
-        outputs.push_back(*it);
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Unable to forward to an Orthanc peer in a Lua script (instance " 
-                   << *it << ", peer " << peer_.GetUrl() << "): " << e.What();
-
-        if (!ignoreExceptions_)
-        {
-          throw;
-        }
-      }
-    }
-
-    return true;
-  }
-}
--- a/OrthancServer/Scheduler/StorePeerCommand.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IServerCommand.h"
-#include "../ServerContext.h"
-#include "../OrthancInitialization.h"
-
-namespace Orthanc
-{
-  class StorePeerCommand : public IServerCommand
-  {
-  private:
-    ServerContext& context_;
-    OrthancPeerParameters peer_;
-    bool ignoreExceptions_;
-
-  public:
-    StorePeerCommand(ServerContext& context,
-                     const OrthancPeerParameters& peer,
-                     bool ignoreExceptions);
-    
-    virtual bool Apply(ListOfStrings& outputs,
-                       const ListOfStrings& inputs);
-  };
-}
--- a/OrthancServer/Scheduler/StoreScuCommand.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,87 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "StoreScuCommand.h"
-
-#include "../../Core/Logging.h"
-
-namespace Orthanc
-{
-  StoreScuCommand::StoreScuCommand(ServerContext& context,
-                                   const std::string& localAet,
-                                   const RemoteModalityParameters& modality,
-                                   bool ignoreExceptions) : 
-    context_(context),
-    modality_(modality),
-    ignoreExceptions_(ignoreExceptions),
-    localAet_(localAet)
-  {
-  }
-
-  bool StoreScuCommand::Apply(ListOfStrings& outputs,
-                             const ListOfStrings& inputs)
-  {
-    ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_);
-
-    for (ListOfStrings::const_iterator
-           it = inputs.begin(); it != inputs.end(); ++it)
-    {
-      LOG(INFO) << "Sending resource " << *it << " to modality \"" 
-                << modality_.GetApplicationEntityTitle() << "\"";
-
-      try
-      {
-        std::string dicom;
-        context_.ReadFile(dicom, *it, FileContentType_Dicom);
-        locker.GetConnection().Store(dicom);
-
-        // Only chain with other commands if this command succeeds
-        outputs.push_back(*it);
-      }
-      catch (OrthancException& e)
-      {
-        // Ignore transmission errors (e.g. if the remote modality is
-        // powered off)
-        LOG(ERROR) << "Unable to forward to a modality in a Lua script (instance " 
-                   << *it << "): " << e.What();
-
-        if (!ignoreExceptions_)
-        {
-          throw;
-        }
-      }
-    }
-
-    return true;
-  }
-}
--- a/OrthancServer/Scheduler/StoreScuCommand.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IServerCommand.h"
-#include "../ServerContext.h"
-
-namespace Orthanc
-{
-  class StoreScuCommand : public IServerCommand
-  {
-  private:
-    ServerContext& context_;
-    RemoteModalityParameters modality_;
-    bool ignoreExceptions_;
-    std::string localAet_;
-
-  public:
-    StoreScuCommand(ServerContext& context,
-                    const std::string& localAet,
-                    const RemoteModalityParameters& modality,
-                    bool ignoreExceptions);
-
-    virtual bool Apply(ListOfStrings& outputs,
-                       const ListOfStrings& inputs);
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/HierarchicalMatcher.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,342 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "HierarchicalMatcher.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/DicomParsing/ToDcmtkBridge.h"
+#include "../OrthancInitialization.h"
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+
+namespace Orthanc
+{
+  HierarchicalMatcher::HierarchicalMatcher(ParsedDicomFile& query)
+  {
+    Setup(*query.GetDcmtkObject().getDataset(), 
+          Configuration::GetGlobalBoolParameter("CaseSensitivePN", false),
+          query.GetEncoding());
+  }
+
+
+  HierarchicalMatcher::~HierarchicalMatcher()
+  {
+    for (Constraints::iterator it = constraints_.begin();
+         it != constraints_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        delete it->second;
+      }
+    }
+
+    for (Sequences::iterator it = sequences_.begin();
+         it != sequences_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        delete it->second;
+      }
+    }
+  }
+
+
+  void HierarchicalMatcher::Setup(DcmItem& dataset,
+                                  bool caseSensitivePN,
+                                  Encoding encoding)
+  {
+    for (unsigned long i = 0; i < dataset.card(); i++)
+    {
+      DcmElement* element = dataset.getElement(i);
+      if (element == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      DicomTag tag(FromDcmtkBridge::Convert(element->getTag()));
+      if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET ||   // Ignore encoding
+          tag.GetElement() == 0x0000)  // Ignore all "Group Length" tags
+      {
+        continue;
+      }
+
+      ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
+
+      if (constraints_.find(tag) != constraints_.end() ||
+          sequences_.find(tag) != sequences_.end())
+      {
+        throw OrthancException(ErrorCode_BadRequest);        
+      }
+
+      if (vr == ValueRepresentation_Sequence)
+      {
+        DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(*element);
+
+        if (sequence.card() == 0 ||
+            (sequence.card() == 1 && sequence.getItem(0)->card() == 0))
+        {
+          // Universal matching of a sequence
+          sequences_[tag] = NULL;
+        }
+        else if (sequence.card() == 1)
+        {
+          sequences_[tag] = new HierarchicalMatcher(*sequence.getItem(0), caseSensitivePN, encoding);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadRequest);        
+        }
+      }
+      else
+      {
+        std::set<DicomTag> ignoreTagLength;
+        std::auto_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement
+                                        (*element, DicomToJsonFlags_None, 
+                                         ORTHANC_MAXIMUM_TAG_LENGTH, encoding, ignoreTagLength));
+
+        if (value->IsBinary())
+        {
+          if (!value->GetContent().empty())
+          {
+            LOG(WARNING) << "This C-Find modality worklist query contains a non-empty tag ("
+                         << tag.Format() << ") with UN (unknown) value representation. "
+                         << "It will be ignored.";
+          }
+
+          constraints_[tag] = NULL;
+        }
+        else if (value->IsNull() ||
+                 value->GetContent().empty())
+        {
+          // This is an universal matcher
+          constraints_[tag] = NULL;
+        }
+        else
+        {
+          // DICOM specifies that searches must be case sensitive, except
+          // for tags with a PN value representation
+          bool sensitive = true;
+          if (vr == ValueRepresentation_PersonName)
+          {
+            sensitive = caseSensitivePN;
+          }
+
+          constraints_[tag] = IFindConstraint::ParseDicomConstraint(tag, value->GetContent(), sensitive);
+        }
+      }
+    }
+  }
+
+
+  std::string HierarchicalMatcher::Format(const std::string& prefix) const
+  {
+    std::string s;
+    
+    for (Constraints::const_iterator it = constraints_.begin();
+         it != constraints_.end(); ++it)
+    {
+      s += prefix + it->first.Format() + " ";
+
+      if (it->second == NULL)
+      {
+        s += "*\n";
+      }
+      else
+      {
+        s += it->second->Format() + "\n";
+      }
+    }
+
+    for (Sequences::const_iterator it = sequences_.begin();
+         it != sequences_.end(); ++it)
+    {
+      s += prefix + it->first.Format() + " ";
+
+      if (it->second == NULL)
+      {
+        s += "*\n";
+      }
+      else
+      {
+        s += "Sequence:\n" + it->second->Format(prefix + "  ");
+      }
+    }
+
+    return s;
+  }
+
+
+  bool HierarchicalMatcher::Match(ParsedDicomFile& dicom) const
+  {
+    return MatchInternal(*dicom.GetDcmtkObject().getDataset(),
+                         dicom.GetEncoding());
+  }
+
+
+  bool HierarchicalMatcher::MatchInternal(DcmItem& item,
+                                          Encoding encoding) const
+  {
+    for (Constraints::const_iterator it = constraints_.begin();
+         it != constraints_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        DcmTagKey tag = ToDcmtkBridge::Convert(it->first);
+
+        DcmElement* element = NULL;
+        if (!item.findAndGetElement(tag, element).good() ||
+            element == NULL)
+        {
+          return false;
+        }
+
+        std::set<DicomTag> ignoreTagLength;
+        std::auto_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement
+                                        (*element, DicomToJsonFlags_None, 
+                                         ORTHANC_MAXIMUM_TAG_LENGTH, encoding, ignoreTagLength));
+
+        if (value->IsNull() ||
+            value->IsBinary() ||
+            !it->second->Match(value->GetContent()))
+        {
+          return false;
+        }
+      }
+    }
+
+    for (Sequences::const_iterator it = sequences_.begin();
+         it != sequences_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        DcmTagKey tag = ToDcmtkBridge::Convert(it->first);
+
+        DcmSequenceOfItems* sequence = NULL;
+        if (!item.findAndGetSequence(tag, sequence).good() ||
+            sequence == NULL)
+        {
+          continue;
+        }
+
+        bool match = false;
+
+        for (unsigned long i = 0; i < sequence->card(); i++)
+        {
+          if (it->second->MatchInternal(*sequence->getItem(i), encoding))
+          {
+            match = true;
+            break;
+          }
+        }
+
+        if (!match)
+        {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+
+
+  DcmDataset* HierarchicalMatcher::ExtractInternal(DcmItem& source,
+                                                   Encoding encoding) const
+  {
+    std::auto_ptr<DcmDataset> target(new DcmDataset);
+
+    for (Constraints::const_iterator it = constraints_.begin();
+         it != constraints_.end(); ++it)
+    {
+      DcmTagKey tag = ToDcmtkBridge::Convert(it->first);
+      
+      DcmElement* element = NULL;
+      if (source.findAndGetElement(tag, element).good() &&
+          element != NULL)
+      {
+        std::auto_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(it->first));
+        cloned->copyFrom(*element);
+        target->insert(cloned.release());
+      }
+    }
+
+    for (Sequences::const_iterator it = sequences_.begin();
+         it != sequences_.end(); ++it)
+    {
+      DcmTagKey tag = ToDcmtkBridge::Convert(it->first);
+
+      DcmSequenceOfItems* sequence = NULL;
+      if (source.findAndGetSequence(tag, sequence).good() &&
+          sequence != NULL)
+      {
+        std::auto_ptr<DcmSequenceOfItems> cloned(new DcmSequenceOfItems(tag));
+
+        for (unsigned long i = 0; i < sequence->card(); i++)
+        {
+          if (it->second == NULL)
+          {
+            cloned->append(new DcmItem(*sequence->getItem(i)));
+          }
+          else if (it->second->MatchInternal(*sequence->getItem(i), encoding))  // TODO Might be optimized
+          {
+            // It is necessary to encapsulate the child dataset into a
+            // "DcmItem" object before it can be included in a
+            // sequence. Otherwise, "dciodvfy" reports an error "Bad
+            // tag in sequence - Expecting Item or Sequence Delimiter."
+            std::auto_ptr<DcmDataset> child(it->second->ExtractInternal(*sequence->getItem(i), encoding));
+            cloned->append(new DcmItem(*child));
+          }
+        }
+
+        target->insert(cloned.release());
+      }
+    }
+
+    return target.release();
+  }
+
+
+  ParsedDicomFile* HierarchicalMatcher::Extract(ParsedDicomFile& dicom) const
+  {
+    std::auto_ptr<DcmDataset> dataset(ExtractInternal(*dicom.GetDcmtkObject().getDataset(),
+                                                      dicom.GetEncoding()));
+
+    std::auto_ptr<ParsedDicomFile> result(new ParsedDicomFile(*dataset));
+    result->SetEncoding(dicom.GetEncoding());
+
+    return result.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/HierarchicalMatcher.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,80 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IFindConstraint.h"
+#include "../../Core/DicomParsing/ParsedDicomFile.h"
+
+class DcmItem;
+
+namespace Orthanc
+{
+  class HierarchicalMatcher : public boost::noncopyable
+  {
+  private:
+    typedef std::map<DicomTag, IFindConstraint*>      Constraints;
+    typedef std::map<DicomTag, HierarchicalMatcher*>  Sequences;
+
+    Constraints  constraints_;
+    Sequences    sequences_;
+
+    void Setup(DcmItem& query,
+               bool caseSensitivePN,
+               Encoding encoding);
+
+    HierarchicalMatcher(DcmItem& query,
+                        bool caseSensitivePN,
+                        Encoding encoding)
+    {
+      Setup(query, caseSensitivePN, encoding);
+    }
+
+    bool MatchInternal(DcmItem& dicom,
+                       Encoding encoding) const;
+
+    DcmDataset* ExtractInternal(DcmItem& dicom,
+                                Encoding encoding) const;
+
+  public:
+    HierarchicalMatcher(ParsedDicomFile& query);
+
+    ~HierarchicalMatcher();
+
+    std::string Format(const std::string& prefix = "") const;
+
+    bool Match(ParsedDicomFile& dicom) const;
+
+    ParsedDicomFile* Extract(ParsedDicomFile& dicom) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/IFindConstraint.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,131 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "IFindConstraint.h"
+
+#include "ListConstraint.h"
+#include "RangeConstraint.h"
+#include "ValueConstraint.h"
+#include "WildcardConstraint.h"
+
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  IFindConstraint* IFindConstraint::ParseDicomConstraint(const DicomTag& tag,
+                                                         const std::string& dicomQuery,
+                                                         bool caseSensitive)
+  {
+    ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
+
+    if (vr == ValueRepresentation_Sequence)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if ((vr == ValueRepresentation_Date ||
+         vr == ValueRepresentation_DateTime ||
+         vr == ValueRepresentation_Time) &&
+        dicomQuery.find('-') != std::string::npos)
+    {
+      /**
+       * Range matching is only defined for TM, DA and DT value
+       * representations. This code fixes issues 35 and 37.
+       *
+       * Reference: "Range matching is not defined for types of
+       * Attributes other than dates and times", DICOM PS 3.4,
+       * C.2.2.2.5 ("Range Matching").
+       **/
+      size_t separator = dicomQuery.find('-');
+      std::string lower = dicomQuery.substr(0, separator);
+      std::string upper = dicomQuery.substr(separator + 1);
+      return new RangeConstraint(lower, upper, caseSensitive);
+    }
+    else if (dicomQuery.find('\\') != std::string::npos)
+    {
+      std::auto_ptr<ListConstraint> constraint(new ListConstraint(caseSensitive));
+
+      std::vector<std::string> items;
+      Toolbox::TokenizeString(items, dicomQuery, '\\');
+
+      for (size_t i = 0; i < items.size(); i++)
+      {
+        constraint->AddAllowedValue(items[i]);
+      }
+
+      return constraint.release();
+    }
+    else if (dicomQuery.find('*') != std::string::npos ||
+             dicomQuery.find('?') != std::string::npos)
+    {
+      return new WildcardConstraint(dicomQuery, caseSensitive);
+    }
+    else
+    {
+      /**
+       * Case-insensitive match for PN value representation (Patient
+       * Name). Case-senstive match for all the other value
+       * representations.
+       *
+       * Reference: DICOM PS 3.4
+       *   - C.2.2.2.1 ("Single Value Matching") 
+       *   - C.2.2.2.4 ("Wild Card Matching")
+       * http://medical.nema.org/Dicom/2011/11_04pu.pdf
+       *
+       * "Except for Attributes with a PN Value Representation, only
+       * entities with values which match exactly the value specified in the
+       * request shall match. This matching is case-sensitive, i.e.,
+       * sensitive to the exact encoding of the key attribute value in
+       * character sets where a letter may have multiple encodings (e.g.,
+       * based on its case, its position in a word, or whether it is
+       * accented)
+       * 
+       * For Attributes with a PN Value Representation (e.g., Patient Name
+       * (0010,0010)), an application may perform literal matching that is
+       * either case-sensitive, or that is insensitive to some or all
+       * aspects of case, position, accent, or other character encoding
+       * variants."
+       *
+       * (0008,0018) UI SOPInstanceUID     => Case-sensitive
+       * (0008,0050) SH AccessionNumber    => Case-sensitive
+       * (0010,0020) LO PatientID          => Case-sensitive
+       * (0020,000D) UI StudyInstanceUID   => Case-sensitive
+       * (0020,000E) UI SeriesInstanceUID  => Case-sensitive
+       **/
+
+      return new ValueConstraint(dicomQuery, caseSensitive);
+    }
+  }
+}
--- a/OrthancServer/Search/IFindConstraint.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/IFindConstraint.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -49,5 +50,11 @@
                        const DicomTag& tag) const = 0;
 
     virtual bool Match(const std::string& value) const = 0;
+
+    virtual std::string Format() const = 0;
+
+    static IFindConstraint* ParseDicomConstraint(const DicomTag& tag,
+                                                 const std::string& dicomQuery,
+                                                 bool caseSensitive);
   };
 }
--- a/OrthancServer/Search/ListConstraint.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/ListConstraint.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -44,9 +45,7 @@
     }
     else
     {
-      std::string s = value;
-      Toolbox::ToUpperCase(s);
-      allowedValues_.insert(s);      
+      allowedValues_.insert(Toolbox::ToUpperCaseWithAccents(value));
     }
   }
 
@@ -66,13 +65,36 @@
 
   bool ListConstraint::Match(const std::string& value) const
   {
-    std::string v = value;
-
-    if (!isCaseSensitive_)
+    std::string s;
+    
+    if (isCaseSensitive_)
     {
-      Toolbox::ToUpperCase(v);
+      s = value;
+    }
+    else
+    {
+      s = Toolbox::ToUpperCaseWithAccents(value);
     }
 
-    return allowedValues_.find(v) != allowedValues_.end();
+    return allowedValues_.find(s) != allowedValues_.end();
+  }
+
+
+  std::string ListConstraint::Format() const
+  {
+    std::string s;
+
+    for (std::set<std::string>::const_iterator
+           it = allowedValues_.begin(); it != allowedValues_.end(); ++it)
+    {
+      if (!s.empty())
+      {
+        s += "\\";
+      }
+
+      s += *it;
+    }
+
+    return s;
   }
 }
--- a/OrthancServer/Search/ListConstraint.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/ListConstraint.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -67,5 +68,7 @@
                        const DicomTag& tag) const;
 
     virtual bool Match(const std::string& value) const;
+
+    virtual std::string Format() const;
   };
 }
--- a/OrthancServer/Search/LookupIdentifierQuery.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/LookupIdentifierQuery.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -35,7 +36,7 @@
 
 #include "../../Core/OrthancException.h"
 #include "SetOfResources.h"
-#include "../FromDcmtkBridge.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
 
 #include <cassert>
 
@@ -43,72 +44,16 @@
 
 namespace Orthanc
 {
-  static const DicomTag patientIdentifiers[] = 
-  {
-    DICOM_TAG_PATIENT_ID,
-    DICOM_TAG_PATIENT_NAME,
-    DICOM_TAG_PATIENT_BIRTH_DATE
-  };
-
-  static const DicomTag studyIdentifiers[] = 
-  {
-    DICOM_TAG_PATIENT_ID,
-    DICOM_TAG_PATIENT_NAME,
-    DICOM_TAG_PATIENT_BIRTH_DATE,
-    DICOM_TAG_STUDY_INSTANCE_UID,
-    DICOM_TAG_ACCESSION_NUMBER,
-    DICOM_TAG_STUDY_DESCRIPTION,
-    DICOM_TAG_STUDY_DATE
-  };
-
-  static const DicomTag seriesIdentifiers[] = 
-  {
-    DICOM_TAG_SERIES_INSTANCE_UID
-  };
-
-  static const DicomTag instanceIdentifiers[] = 
-  {
-    DICOM_TAG_SOP_INSTANCE_UID
-  };
-
-
-  void LookupIdentifierQuery::LoadIdentifiers(const DicomTag*& tags,
-                                              size_t& size,
-                                              ResourceType level)
-  {
-    switch (level)
-    {
-      case ResourceType_Patient:
-        tags = patientIdentifiers;
-        size = sizeof(patientIdentifiers) / sizeof(DicomTag);
-        break;
-
-      case ResourceType_Study:
-        tags = studyIdentifiers;
-        size = sizeof(studyIdentifiers) / sizeof(DicomTag);
-        break;
-
-      case ResourceType_Series:
-        tags = seriesIdentifiers;
-        size = sizeof(seriesIdentifiers) / sizeof(DicomTag);
-        break;
-
-      case ResourceType_Instance:
-        tags = instanceIdentifiers;
-        size = sizeof(instanceIdentifiers) / sizeof(DicomTag);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
   LookupIdentifierQuery::Disjunction::~Disjunction()
   {
-    for (size_t i = 0; i < disjunction_.size(); i++)
+    for (size_t i = 0; i < singleConstraints_.size(); i++)
     {
-      delete disjunction_[i];
+      delete singleConstraints_[i];
+    }
+
+    for (size_t i = 0; i < rangeConstraints_.size(); i++)
+    {
+      delete rangeConstraints_[i];
     }
   }
 
@@ -117,87 +62,52 @@
                                                IdentifierConstraintType type,
                                                const std::string& value)
   {
-    disjunction_.push_back(new Constraint(tag, type, value));
+    singleConstraints_.push_back(new SingleConstraint(tag, type, value));
+  }
+
+
+  void LookupIdentifierQuery::Disjunction::AddRange(const DicomTag& tag,
+                                                    const std::string& start,
+                                                    const std::string& end)
+  {
+    rangeConstraints_.push_back(new RangeConstraint(tag, start, end));
   }
 
 
   LookupIdentifierQuery::~LookupIdentifierQuery()
   {
-    for (Constraints::iterator it = constraints_.begin();
-         it != constraints_.end(); ++it)
+    for (Disjunctions::iterator it = disjunctions_.begin();
+         it != disjunctions_.end(); ++it)
     {
       delete *it;
     }
   }
 
 
-
-  bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag,
-                                           ResourceType level)
-  {
-    const DicomTag* tags;
-    size_t size;
-
-    LoadIdentifiers(tags, size, level);
-
-    for (size_t i = 0; i < size; i++)
-    {
-      if (tag == tags[i])
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-
   void LookupIdentifierQuery::AddConstraint(DicomTag tag,
                                             IdentifierConstraintType type,
                                             const std::string& value)
   {
     assert(IsIdentifier(tag));
-    constraints_.push_back(new Disjunction);
-    constraints_.back()->Add(tag, type, value);
+    disjunctions_.push_back(new Disjunction);
+    disjunctions_.back()->Add(tag, type, value);
+  }
+
+
+  void LookupIdentifierQuery::AddRange(DicomTag tag,
+                                       const std::string& start,
+                                       const std::string& end)
+  {
+    assert(IsIdentifier(tag));
+    disjunctions_.push_back(new Disjunction);
+    disjunctions_.back()->AddRange(tag, start, end);
   }
 
 
   LookupIdentifierQuery::Disjunction& LookupIdentifierQuery::AddDisjunction()
   {
-    constraints_.push_back(new Disjunction);
-    return *constraints_.back();
-  }
-
-
-  std::string LookupIdentifierQuery::NormalizeIdentifier(const std::string& value)
-  {
-    std::string s = Toolbox::ConvertToAscii(Toolbox::StripSpaces(value));
-    Toolbox::ToUpperCase(s);
-    return s;
-  }
-
-
-  void LookupIdentifierQuery::StoreIdentifiers(IDatabaseWrapper& database,
-                                               int64_t resource,
-                                               ResourceType level,
-                                               const DicomMap& map)
-  {
-    const DicomTag* tags;
-    size_t size;
-
-    LoadIdentifiers(tags, size, level);
-
-    for (size_t i = 0; i < size; i++)
-    {
-      const DicomValue* value = map.TestAndGetValue(tags[i]);
-      if (value != NULL &&
-          !value->IsNull() &&
-          !value->IsBinary())
-      {
-        std::string s = NormalizeIdentifier(value->GetContent());
-        database.SetIdentifierTag(resource, tags[i], s);
-      }
-    }
+    disjunctions_.push_back(new Disjunction);
+    return *disjunctions_.back();
   }
 
 
@@ -214,15 +124,26 @@
   void LookupIdentifierQuery::Apply(SetOfResources& result,
                                     IDatabaseWrapper& database)
   {
-    for (size_t i = 0; i < GetSize(); i++)
+    for (size_t i = 0; i < disjunctions_.size(); i++)
     {
       std::list<int64_t> a;
 
-      for (size_t j = 0; j < constraints_[i]->GetSize(); j++)
+      for (size_t j = 0; j < disjunctions_[i]->GetSingleConstraintsCount(); j++)
       {
-        const Constraint& constraint = constraints_[i]->GetConstraint(j);
+        const SingleConstraint& constraint = disjunctions_[i]->GetSingleConstraint(j);
         std::list<int64_t> b;
-        database.LookupIdentifier(b, level_, constraint.GetTag(), constraint.GetType(), constraint.GetValue());
+        database.LookupIdentifier(b, level_, constraint.GetTag(), 
+                                  constraint.GetType(), constraint.GetValue());
+
+        a.splice(a.end(), b);
+      }
+
+      for (size_t j = 0; j < disjunctions_[i]->GetRangeConstraintsCount(); j++)
+      {
+        const RangeConstraint& constraint = disjunctions_[i]->GetRangeConstraint(j);
+        std::list<int64_t> b;
+        database.LookupIdentifierRange(b, level_, constraint.GetTag(), 
+                                       constraint.GetStart(), constraint.GetEnd());
 
         a.splice(a.end(), b);
       }
@@ -235,18 +156,18 @@
   void LookupIdentifierQuery::Print(std::ostream& s) const
   {
     s << "Constraint: " << std::endl;
-    for (Constraints::const_iterator
-           it = constraints_.begin(); it != constraints_.end(); ++it)
+    for (Disjunctions::const_iterator
+           it = disjunctions_.begin(); it != disjunctions_.end(); ++it)
     {
-      if (it == constraints_.begin())
+      if (it == disjunctions_.begin())
         s << "   ";
       else
         s << "OR ";
 
-      for (size_t j = 0; j < (*it)->GetSize(); j++)
+      for (size_t j = 0; j < (*it)->GetSingleConstraintsCount(); j++)
       {
-        const Constraint& c = (*it)->GetConstraint(j);
-        s << FromDcmtkBridge::GetName(c.GetTag());
+        const SingleConstraint& c = (*it)->GetSingleConstraint(j);
+        s << FromDcmtkBridge::GetTagName(c.GetTag(), "");
 
         switch (c.GetType())
         {
--- a/OrthancServer/Search/LookupIdentifierQuery.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/LookupIdentifierQuery.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,7 +33,7 @@
 
 #pragma once
 
-#include "../ServerEnumerations.h"
+#include "../ServerToolbox.h"
 #include "../IDatabaseWrapper.h"
 
 #include "SetOfResources.h"
@@ -64,8 +65,11 @@
 
   class LookupIdentifierQuery : public boost::noncopyable
   {
+    // This class encodes a conjunction ("AND") of disjunctions. Each
+    // disjunction represents an "OR" of several constraints.
+
   public:
-    class Constraint
+    class SingleConstraint
     {
     private:
       DicomTag                  tag_;
@@ -73,12 +77,12 @@
       std::string               value_;
 
     public:
-      Constraint(const DicomTag& tag,
-                 IdentifierConstraintType type,
-                 const std::string& value) : 
+      SingleConstraint(const DicomTag& tag,
+                       IdentifierConstraintType type,
+                       const std::string& value) : 
         tag_(tag),
         type_(type),
-        value_(NormalizeIdentifier(value))
+        value_(ServerToolbox::NormalizeIdentifier(value))
       {
       }
 
@@ -99,10 +103,45 @@
     };
 
 
+    class RangeConstraint
+    {
+    private:
+      DicomTag     tag_;
+      std::string  start_;
+      std::string  end_;
+
+    public:
+      RangeConstraint(const DicomTag& tag,
+                      const std::string& start,
+                      const std::string& end) : 
+        tag_(tag),
+        start_(ServerToolbox::NormalizeIdentifier(start)),
+        end_(ServerToolbox::NormalizeIdentifier(end))
+      {
+      }
+
+      const DicomTag& GetTag() const
+      {
+        return tag_;
+      }
+
+      const std::string& GetStart() const
+      {
+        return start_;
+      }
+
+      const std::string& GetEnd() const
+      {
+        return end_;
+      }
+    };
+
+
     class Disjunction : public boost::noncopyable
     {
     private:
-      std::vector<Constraint*>  disjunction_;
+      std::vector<SingleConstraint*>  singleConstraints_;
+      std::vector<RangeConstraint*>   rangeConstraints_;
 
     public:
       ~Disjunction();
@@ -111,25 +150,37 @@
                IdentifierConstraintType type,
                const std::string& value);
 
-      size_t GetSize() const
+      void AddRange(const DicomTag& tag,
+                    const std::string& start,
+                    const std::string& end);
+
+      size_t GetSingleConstraintsCount() const
       {
-        return disjunction_.size();
+        return singleConstraints_.size();
       }
 
-      const Constraint&  GetConstraint(size_t i) const
+      const SingleConstraint&  GetSingleConstraint(size_t i) const
+      {
+        return *singleConstraints_[i];
+      }
+
+      size_t GetRangeConstraintsCount() const
       {
-        return *disjunction_[i];
+        return rangeConstraints_.size();
+      }
+
+      const RangeConstraint&  GetRangeConstraint(size_t i) const
+      {
+        return *rangeConstraints_[i];
       }
     };
 
 
   private:
-    typedef std::vector<Disjunction*>  Constraints;
+    typedef std::vector<Disjunction*>  Disjunctions;
 
     ResourceType  level_;
-    Constraints   constraints_;
-
-    static std::string NormalizeIdentifier(const std::string& value);
+    Disjunctions  disjunctions_;
 
   public:
     LookupIdentifierQuery(ResourceType level) : level_(level)
@@ -140,13 +191,17 @@
 
     bool IsIdentifier(const DicomTag& tag)
     {
-      return IsIdentifier(tag, level_);
+      return ServerToolbox::IsIdentifier(tag, level_);
     }
 
     void AddConstraint(DicomTag tag,
                        IdentifierConstraintType type,
                        const std::string& value);
 
+    void AddRange(DicomTag tag,
+                  const std::string& start,
+                  const std::string& end);
+
     Disjunction& AddDisjunction();
 
     ResourceType GetLevel() const
@@ -154,11 +209,6 @@
       return level_;
     }
 
-    size_t GetSize() const
-    {
-      return constraints_.size();
-    }
-
     // The database must be locked
     void Apply(std::list<std::string>& result,
                IDatabaseWrapper& database);
@@ -166,18 +216,6 @@
     void Apply(SetOfResources& result,
                IDatabaseWrapper& database);
 
-    static void LoadIdentifiers(const DicomTag*& tags,
-                                size_t& size,
-                                ResourceType level);
-
-    static bool IsIdentifier(const DicomTag& tag,
-                             ResourceType level);
-
-    static void StoreIdentifiers(IDatabaseWrapper& database,
-                                 int64_t resource,
-                                 ResourceType level,
-                                 const DicomMap& map);
-
     void Print(std::ostream& s) const;
   };
 }
--- a/OrthancServer/Search/LookupResource.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/LookupResource.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,15 +34,10 @@
 #include "../PrecompiledHeadersServer.h"
 #include "LookupResource.h"
 
-#include "ListConstraint.h"
-#include "RangeConstraint.h"
-#include "ValueConstraint.h"
-#include "WildcardConstraint.h"
-
 #include "../../Core/OrthancException.h"
 #include "../../Core/FileStorage/StorageAccessor.h"
 #include "../ServerToolbox.h"
-#include "../FromDcmtkBridge.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
 
 
 namespace Orthanc
@@ -51,7 +47,7 @@
     const DicomTag* tags = NULL;
     size_t size;
     
-    LookupIdentifierQuery::LoadIdentifiers(tags, size, level);
+    ServerToolbox::LoadIdentifiers(tags, size, level);
     
     for (size_t i = 0; i < size; i++)
     {
@@ -130,16 +126,16 @@
         levels_[ResourceType_Patient] = new Level(ResourceType_Patient);
         break;
 
-      case ResourceType_Study:
-        levels_[ResourceType_Study] = new Level(ResourceType_Study);
+      case ResourceType_Instance:
+        levels_[ResourceType_Instance] = new Level(ResourceType_Instance);
         // Do not add "break" here
 
       case ResourceType_Series:
         levels_[ResourceType_Series] = new Level(ResourceType_Series);
         // Do not add "break" here
 
-      case ResourceType_Instance:
-        levels_[ResourceType_Instance] = new Level(ResourceType_Instance);
+      case ResourceType_Study:
+        levels_[ResourceType_Study] = new Level(ResourceType_Study);
         break;
 
       default:
@@ -426,85 +422,16 @@
                                           const std::string& dicomQuery,
                                           bool caseSensitive)
   {
-    ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag);
-
     // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
     // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
     if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
     {
       SetModalitiesInStudy(dicomQuery);
     }
-    else if ((vr == ValueRepresentation_Date ||
-              vr == ValueRepresentation_DateTime ||
-              vr == ValueRepresentation_Time) &&
-             dicomQuery.find('-') != std::string::npos)
-    {
-      /**
-       * Range matching is only defined for TM, DA and DT value
-       * representations. This code fixes issues 35 and 37.
-       *
-       * Reference: "Range matching is not defined for types of
-       * Attributes other than dates and times", DICOM PS 3.4,
-       * C.2.2.2.5 ("Range Matching").
-       **/
-      size_t separator = dicomQuery.find('-');
-      std::string lower = dicomQuery.substr(0, separator);
-      std::string upper = dicomQuery.substr(separator + 1);
-      Add(tag, new RangeConstraint(lower, upper, caseSensitive));
-    }
-    else if (dicomQuery.find('\\') != std::string::npos)
-    {
-      std::auto_ptr<ListConstraint> constraint(new ListConstraint(caseSensitive));
-
-      std::vector<std::string> items;
-      Toolbox::TokenizeString(items, dicomQuery, '\\');
-
-      for (size_t i = 0; i < items.size(); i++)
-      {
-        constraint->AddAllowedValue(items[i]);
-      }
-
-      Add(tag, constraint.release());
-    }
-    else if (dicomQuery.find('*') != std::string::npos ||
-             dicomQuery.find('?') != std::string::npos)
+    else 
     {
-      Add(tag, new WildcardConstraint(dicomQuery, caseSensitive));
-    }
-    else
-    {
-      /**
-       * Case-insensitive match for PN value representation (Patient
-       * Name). Case-senstive match for all the other value
-       * representations.
-       *
-       * Reference: DICOM PS 3.4
-       *   - C.2.2.2.1 ("Single Value Matching") 
-       *   - C.2.2.2.4 ("Wild Card Matching")
-       * http://medical.nema.org/Dicom/2011/11_04pu.pdf
-       *
-       * "Except for Attributes with a PN Value Representation, only
-       * entities with values which match exactly the value specified in the
-       * request shall match. This matching is case-sensitive, i.e.,
-       * sensitive to the exact encoding of the key attribute value in
-       * character sets where a letter may have multiple encodings (e.g.,
-       * based on its case, its position in a word, or whether it is
-       * accented)
-       * 
-       * For Attributes with a PN Value Representation (e.g., Patient Name
-       * (0010,0010)), an application may perform literal matching that is
-       * either case-sensitive, or that is insensitive to some or all
-       * aspects of case, position, accent, or other character encoding
-       * variants."
-       *
-       * (0008,0018) UI SOPInstanceUID     => Case-sensitive
-       * (0008,0050) SH AccessionNumber    => Case-sensitive
-       * (0010,0020) LO PatientID          => Case-sensitive
-       * (0020,000D) UI StudyInstanceUID   => Case-sensitive
-       * (0020,000E) UI SeriesInstanceUID  => Case-sensitive
-      **/
-
-      Add(tag, new ValueConstraint(dicomQuery, caseSensitive));
+      Add(tag, IFindConstraint::ParseDicomConstraint(tag, dicomQuery, caseSensitive));
     }
   }
+
 }
--- a/OrthancServer/Search/LookupResource.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/LookupResource.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/Search/RangeConstraint.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/RangeConstraint.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -40,14 +41,17 @@
   RangeConstraint::RangeConstraint(const std::string& lower,
                                    const std::string& upper,
                                    bool isCaseSensitive) : 
-    lower_(lower),
-    upper_(upper),
     isCaseSensitive_(isCaseSensitive)
   {
-    if (!isCaseSensitive_)
+    if (isCaseSensitive_)
     {
-      Toolbox::ToUpperCase(lower_);
-      Toolbox::ToUpperCase(upper_);
+      lower_ = lower;
+      upper_ = upper;
+    }
+    else
+    {
+      lower_ = Toolbox::ToUpperCaseWithAccents(lower);
+      upper_ = Toolbox::ToUpperCaseWithAccents(upper);
     }
   }
 
@@ -55,18 +59,37 @@
   void RangeConstraint::Setup(LookupIdentifierQuery& lookup,
                               const DicomTag& tag) const
   {
-    lookup.AddConstraint(tag, IdentifierConstraintType_GreaterOrEqual, lower_);
-    lookup.AddConstraint(tag, IdentifierConstraintType_SmallerOrEqual, upper_);
+    if (!lower_.empty() &&
+        !upper_.empty())
+    {
+      lookup.AddRange(tag, lower_, upper_);
+    }
+    else
+    {
+      if (!lower_.empty())
+      {
+        lookup.AddConstraint(tag, IdentifierConstraintType_GreaterOrEqual, lower_);
+      }
+
+      if (!upper_.empty())
+      {
+        lookup.AddConstraint(tag, IdentifierConstraintType_SmallerOrEqual, upper_);
+      }
+    }
   }
 
 
   bool RangeConstraint::Match(const std::string& value) const
   {
-    std::string v = value;
+    std::string v;
 
-    if (!isCaseSensitive_)
+    if (isCaseSensitive_)
     {
-      Toolbox::ToUpperCase(v);
+      v = value;
+    }
+    else
+    {
+      v = Toolbox::ToUpperCaseWithAccents(value);
     }
 
     if (lower_.size() == 0 && 
--- a/OrthancServer/Search/RangeConstraint.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/RangeConstraint.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -64,5 +65,10 @@
                        const DicomTag& tag) const;
 
     virtual bool Match(const std::string& value) const;
+
+    virtual std::string Format() const
+    {
+      return lower_ + "-" + upper_;
+    }
   };
 }
--- a/OrthancServer/Search/SetOfResources.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/SetOfResources.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/Search/SetOfResources.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/SetOfResources.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/Search/ValueConstraint.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/ValueConstraint.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -41,12 +42,15 @@
 {
   ValueConstraint::ValueConstraint(const std::string& value,
                                    bool isCaseSensitive) : 
-    value_(value),
     isCaseSensitive_(isCaseSensitive)
   {
-    if (!isCaseSensitive)
+    if (isCaseSensitive)
     {
-      Toolbox::ToUpperCase(value_);
+      value_ = value;
+    }
+    else
+    {
+      value_ = Toolbox::ToUpperCaseWithAccents(value);
     }
   }
 
@@ -65,9 +69,7 @@
     }
     else
     {
-      std::string v;
-      Toolbox::ToUpperCase(v, value);
-      return value_ == v;
+      return value_ == Toolbox::ToUpperCaseWithAccents(value);
     }
   }
 }
--- a/OrthancServer/Search/ValueConstraint.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/ValueConstraint.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -61,5 +62,10 @@
                        const DicomTag& tag) const;
 
     virtual bool Match(const std::string& value) const;
+
+    virtual std::string Format() const
+    {
+      return value_;
+    }
   };
 }
--- a/OrthancServer/Search/WildcardConstraint.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/WildcardConstraint.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -41,6 +42,24 @@
   {
     boost::regex  pattern_;
     std::string   wildcard_;
+    bool          isCaseSensitive_;
+
+    PImpl(const std::string& wildcard,
+          bool isCaseSensitive)
+    {
+      isCaseSensitive_ = isCaseSensitive;
+    
+      if (isCaseSensitive)
+      {
+        wildcard_ = wildcard;
+      }
+      else
+      {
+        wildcard_ = Toolbox::ToUpperCaseWithAccents(wildcard);
+      }
+
+      pattern_ = boost::regex(Toolbox::WildcardToRegularExpression(wildcard_));
+    }
   };
 
 
@@ -52,25 +71,20 @@
 
   WildcardConstraint::WildcardConstraint(const std::string& wildcard,
                                          bool isCaseSensitive) :
-    pimpl_(new PImpl)
+    pimpl_(new PImpl(wildcard, isCaseSensitive))
   {
-    pimpl_->wildcard_ = wildcard;
-
-    std::string re = Toolbox::WildcardToRegularExpression(wildcard);
-
-    if (isCaseSensitive)
-    {
-      pimpl_->pattern_ = boost::regex(re);
-    }
-    else
-    {
-      pimpl_->pattern_ = boost::regex(re, boost::regex::icase /* case insensitive search */);
-    }
   }
 
   bool WildcardConstraint::Match(const std::string& value) const
   {
-    return boost::regex_match(value, pimpl_->pattern_);
+    if (pimpl_->isCaseSensitive_)
+    {
+      return boost::regex_match(value, pimpl_->pattern_);
+    }
+    else
+    {
+      return boost::regex_match(Toolbox::ToUpperCaseWithAccents(value), pimpl_->pattern_);
+    }
   }
 
   void WildcardConstraint::Setup(LookupIdentifierQuery& lookup,
@@ -78,4 +92,9 @@
   {
     lookup.AddConstraint(tag, IdentifierConstraintType_Wildcard, pimpl_->wildcard_);
   }
+
+  std::string WildcardConstraint::Format() const
+  {
+    return pimpl_->wildcard_;
+  }
 }
--- a/OrthancServer/Search/WildcardConstraint.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/Search/WildcardConstraint.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -59,5 +60,7 @@
                        const DicomTag& tag) const;
 
     virtual bool Match(const std::string& value) const;
+
+    virtual std::string Format() const;
   };
 }
--- a/OrthancServer/ServerContext.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/ServerContext.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,26 +34,22 @@
 #include "PrecompiledHeadersServer.h"
 #include "ServerContext.h"
 
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/FileStorage/StorageAccessor.h"
 #include "../Core/HttpServer/FilesystemHttpSender.h"
 #include "../Core/HttpServer/HttpStreamTranscoder.h"
 #include "../Core/Logging.h"
-#include "FromDcmtkBridge.h"
+#include "../Plugins/Engine/OrthancPlugins.h"
+#include "OrthancInitialization.h"
+#include "OrthancRestApi/OrthancRestApi.h"
+#include "Search/LookupResource.h"
+#include "ServerJobs/OrthancJobUnserializer.h"
 #include "ServerToolbox.h"
-#include "OrthancInitialization.h"
 
 #include <EmbeddedResources.h>
 #include <dcmtk/dcmdata/dcfilefo.h>
 
 
-#include "Scheduler/CallSystemCommand.h"
-#include "Scheduler/DeleteInstanceCommand.h"
-#include "Scheduler/ModifyInstanceCommand.h"
-#include "Scheduler/StoreScuCommand.h"
-#include "Scheduler/StorePeerCommand.h"
-#include "OrthancRestApi/OrthancRestApi.h"
-#include "../Plugins/Engine/OrthancPlugins.h"
-
 
 #define ENABLE_DICOM_CACHE  1
 
@@ -69,11 +66,12 @@
 
 namespace Orthanc
 {
-  void ServerContext::ChangeThread(ServerContext* that)
+  void ServerContext::ChangeThread(ServerContext* that,
+                                   unsigned int sleepDelay)
   {
     while (!that->done_)
     {
-      std::auto_ptr<IDynamicObject> obj(that->pendingChanges_.Dequeue(100));
+      std::auto_ptr<IDynamicObject> obj(that->pendingChanges_.Dequeue(sleepDelay));
         
       if (obj.get() != NULL)
       {
@@ -85,12 +83,24 @@
         {
           try
           {
-            it->GetListener().SignalChange(change);
+            try
+            {
+              it->GetListener().SignalChange(change);
+            }
+            catch (std::bad_alloc&)
+            {
+              LOG(ERROR) << "Not enough memory while signaling a change";
+            }
+            catch (...)
+            {
+              throw OrthancException(ErrorCode_InternalError);
+            }
           }
           catch (OrthancException& e)
           {
             LOG(ERROR) << "Error in the " << it->GetDescription() 
-                       << " callback while signaling a change: " << e.What();
+                       << " callback while signaling a change: " << e.What()
+                       << " (code " << e.GetErrorCode() << ")";
           }
         }
       }
@@ -98,29 +108,139 @@
   }
 
 
+  void ServerContext::SaveJobsThread(ServerContext* that,
+                                     unsigned int sleepDelay)
+  {
+    static const boost::posix_time::time_duration PERIODICITY =
+      boost::posix_time::seconds(10);
+    
+    boost::posix_time::ptime next =
+      boost::posix_time::microsec_clock::universal_time() + PERIODICITY;
+    
+    while (!that->done_)
+    {
+      boost::this_thread::sleep(boost::posix_time::milliseconds(sleepDelay));
+
+      if (that->haveJobsChanged_ ||
+          boost::posix_time::microsec_clock::universal_time() >= next)
+      {
+        that->haveJobsChanged_ = false;
+        that->SaveJobsEngine();
+        next = boost::posix_time::microsec_clock::universal_time() + PERIODICITY;
+      }
+    }
+  }
+  
+
+  void ServerContext::SignalJobSubmitted(const std::string& jobId)
+  {
+    haveJobsChanged_ = true;
+    mainLua_.SignalJobSubmitted(jobId);
+  }
+  
+
+  void ServerContext::SignalJobSuccess(const std::string& jobId)
+  {
+    haveJobsChanged_ = true;
+    mainLua_.SignalJobSuccess(jobId);
+  }
+
+  
+  void ServerContext::SignalJobFailure(const std::string& jobId)
+  {
+    haveJobsChanged_ = true;
+    mainLua_.SignalJobFailure(jobId);
+  }
+
+
+  void ServerContext::SetupJobsEngine(bool unitTesting,
+                                      bool loadJobsFromDatabase)
+  {
+    if (loadJobsFromDatabase)
+    {
+      std::string serialized;
+      if (index_.LookupGlobalProperty(serialized, GlobalProperty_JobsRegistry))
+      {
+        LOG(WARNING) << "Reloading the jobs from the last execution of Orthanc";
+        OrthancJobUnserializer unserializer(*this);
+
+        try
+        {
+          jobsEngine_.LoadRegistryFromString(unserializer, serialized);
+        }
+        catch (OrthancException& e)
+        {
+          LOG(ERROR) << "Cannot unserialize the jobs engine: " << e.What();
+          throw;
+        }
+      }
+      else
+      {
+        LOG(INFO) << "The last execution of Orthanc has archived no job";
+      }
+    }
+    else
+    {
+      LOG(WARNING) << "Not reloading the jobs from the last execution of Orthanc";
+    }
+
+    //jobsEngine_.GetRegistry().SetMaxCompleted   // TODO
+
+    jobsEngine_.GetRegistry().SetObserver(*this);
+    jobsEngine_.Start();
+    isJobsEngineUnserialized_ = true;
+
+    saveJobsThread_ = boost::thread(SaveJobsThread, this, (unitTesting ? 20 : 100));
+  }
+
+
+  void ServerContext::SaveJobsEngine()
+  {
+    VLOG(1) << "Serializing the content of the jobs engine";
+    
+    try
+    {
+      Json::Value value;
+      jobsEngine_.GetRegistry().Serialize(value);
+
+      Json::FastWriter writer;
+      std::string serialized = writer.write(value);
+
+      index_.SetGlobalProperty(GlobalProperty_JobsRegistry, serialized);
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Cannot serialize the jobs engine: " << e.What();
+    }
+  }
+
+
   ServerContext::ServerContext(IDatabaseWrapper& database,
-                               IStorageArea& area) :
-    index_(*this, database),
+                               IStorageArea& area,
+                               bool unitTesting) :
+    index_(*this, database, (unitTesting ? 20 : 500)),
     area_(area),
     compressionEnabled_(false),
     storeMD5_(true),
     provider_(*this),
     dicomCache_(provider_, DICOM_CACHE_SIZE),
-    scheduler_(Configuration::GetGlobalIntegerParameter("LimitJobs", 10)),
-    lua_(*this),
-#if ORTHANC_PLUGINS_ENABLED == 1
+    mainLua_(*this),
+    filterLua_(*this),
+    luaListener_(*this),
+#if ORTHANC_ENABLE_PLUGINS == 1
     plugins_(NULL),
 #endif
     done_(false),
-    queryRetrieveArchive_(Configuration::GetGlobalIntegerParameter("QueryRetrieveSize", 10)),
+    haveJobsChanged_(false),
+    isJobsEngineUnserialized_(false),
+    queryRetrieveArchive_(Configuration::GetGlobalUnsignedIntegerParameter("QueryRetrieveSize", 10)),
     defaultLocalAet_(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"))
   {
-    uint64_t s = Configuration::GetGlobalIntegerParameter("DicomAssociationCloseDelay", 5);  // In seconds
-    scu_.SetMillisecondsBeforeClose(s * 1000);  // Milliseconds are expected here
+    jobsEngine_.SetWorkersCount(Configuration::GetGlobalUnsignedIntegerParameter("ConcurrentJobs", 2));
+    jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
 
-    listeners_.push_back(ServerListener(lua_, "Lua"));
-
-    changeThread_ = boost::thread(ChangeThread, this);
+    listeners_.push_back(ServerListener(luaListener_, "Lua"));
+    changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100));
   }
 
 
@@ -151,10 +271,21 @@
         changeThread_.join();
       }
 
-      scu_.Finalize();
+      if (saveJobsThread_.joinable())
+      {
+        saveJobsThread_.join();
+      }
+
+      jobsEngine_.GetRegistry().ResetObserver();
+
+      if (isJobsEngineUnserialized_)
+      {
+        // Avoid losing jobs if the JobsRegistry cannot be unserialized
+        SaveJobsEngine();
+      }
 
       // Do not change the order below!
-      scheduler_.Stop();
+      jobsEngine_.Stop();
       index_.Stop();
     }
   }
@@ -189,7 +320,7 @@
       resultPublicId = hasher.HashInstance();
 
       Json::Value simplifiedTags;
-      Toolbox::SimplifyTags(simplifiedTags, dicom.GetJson());
+      ServerToolbox::SimplifyTags(simplifiedTags, dicom.GetJson(), DicomToJsonFormat_Human);
 
       // Test if the instance must be filtered out
       bool accepted = true;
@@ -210,7 +341,8 @@
           catch (OrthancException& e)
           {
             LOG(ERROR) << "Error in the " << it->GetDescription() 
-                       << " callback while receiving an instance: " << e.What();
+                       << " callback while receiving an instance: " << e.What()
+                       << " (code " << e.GetErrorCode() << ")";
             throw;
           }
         }
@@ -222,6 +354,13 @@
         return StoreStatus_FilteredOut;
       }
 
+      {
+        // Remove the file from the DicomCache (useful if
+        // "OverwriteInstances" is set to "true")
+        boost::mutex::scoped_lock lock(dicomCacheMutex_);
+        dicomCache_.Invalidate(resultPublicId);
+      }
+
       // TODO Should we use "gzip" instead?
       CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
 
@@ -236,8 +375,7 @@
 
       typedef std::map<MetadataType, std::string>  InstanceMetadata;
       InstanceMetadata  instanceMetadata;
-      StoreStatus status = index_.Store(instanceMetadata, dicom.GetSummary(), attachments, 
-                                        dicom.GetRemoteAet(), dicom.GetMetadata());
+      StoreStatus status = index_.Store(instanceMetadata, dicom, attachments);
 
       // Only keep the metadata for the "instance" level
       dicom.GetMetadata().clear();
@@ -288,7 +426,8 @@
           catch (OrthancException& e)
           {
             LOG(ERROR) << "Error in the " << it->GetDescription() 
-                       << " callback while receiving an instance: " << e.What();
+                       << " callback while receiving an instance: " << e.What()
+                       << " (code " << e.GetErrorCode() << ")";
           }
         }
       }
@@ -299,7 +438,7 @@
     {
       if (e.GetErrorCode() == ErrorCode_InexistentTag)
       {
-        Toolbox::LogMissingRequiredTag(dicom.GetSummary());
+        dicom.GetSummary().LogMissingTagsForStore();
       }
 
       throw;
@@ -318,7 +457,7 @@
     }
 
     StorageAccessor accessor(area_);
-    accessor.AnswerFile(output, attachment);
+    accessor.AnswerFile(output, attachment, GetFileContentMime(content));
   }
 
 
@@ -368,35 +507,101 @@
   }
 
 
-  void ServerContext::ReadJson(Json::Value& result,
-                               const std::string& instancePublicId)
+  void ServerContext::ReadDicomAsJsonInternal(std::string& result,
+                                              const std::string& instancePublicId)
   {
-    std::string s;
-    ReadFile(s, instancePublicId, FileContentType_DicomAsJson);
+    FileInfo attachment;
+    if (index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomAsJson))
+    {
+      ReadAttachment(result, attachment);
+    }
+    else
+    {
+      // The "DICOM as JSON" summary is not available from the Orthanc
+      // store (most probably deleted), reconstruct it from the DICOM file
+      std::string dicom;
+      ReadDicom(dicom, instancePublicId);
 
-    Json::Reader reader;
-    if (!reader.parse(s, result))
-    {
-      throw OrthancException(ErrorCode_CorruptedFile);
+      LOG(INFO) << "Reconstructing the missing DICOM-as-JSON summary for instance: "
+                << instancePublicId;
+    
+      ParsedDicomFile parsed(dicom);
+
+      Json::Value summary;
+      parsed.DatasetToJson(summary);
+
+      result = summary.toStyledString();
+
+      if (!AddAttachment(instancePublicId, FileContentType_DicomAsJson,
+                         result.c_str(), result.size()))
+      {
+        LOG(WARNING) << "Cannot associate the DICOM-as-JSON summary to instance: " << instancePublicId;
+        throw OrthancException(ErrorCode_InternalError);
+      }
     }
   }
 
 
-  void ServerContext::ReadFile(std::string& result,
-                               const std::string& instancePublicId,
-                               FileContentType content,
-                               bool uncompressIfNeeded)
+  void ServerContext::ReadDicomAsJson(std::string& result,
+                                      const std::string& instancePublicId,
+                                      const std::set<DicomTag>& ignoreTagLength)
+  {
+    if (ignoreTagLength.empty())
+    {
+      ReadDicomAsJsonInternal(result, instancePublicId);
+    }
+    else
+    {
+      Json::Value tmp;
+      ReadDicomAsJson(tmp, instancePublicId, ignoreTagLength);
+      result = tmp.toStyledString();
+    }
+  }
+
+
+  void ServerContext::ReadDicomAsJson(Json::Value& result,
+                                      const std::string& instancePublicId,
+                                      const std::set<DicomTag>& ignoreTagLength)
+  {
+    if (ignoreTagLength.empty())
+    {
+      std::string tmp;
+      ReadDicomAsJsonInternal(tmp, instancePublicId);
+
+      Json::Reader reader;
+      if (!reader.parse(tmp, result))
+      {
+        throw OrthancException(ErrorCode_CorruptedFile);
+      }
+    }
+    else
+    {
+      // The "DicomAsJson" attachment might have stored some tags as
+      // "too long". We are forced to re-parse the DICOM file.
+      std::string dicom;
+      ReadDicom(dicom, instancePublicId);
+
+      ParsedDicomFile parsed(dicom);
+      parsed.DatasetToJson(result, ignoreTagLength);
+    }
+  }
+
+
+  void ServerContext::ReadAttachment(std::string& result,
+                                     const std::string& instancePublicId,
+                                     FileContentType content,
+                                     bool uncompressIfNeeded)
   {
     FileInfo attachment;
     if (!index_.LookupAttachment(attachment, instancePublicId, content))
     {
+      LOG(WARNING) << "Unable to read attachment " << EnumerationToString(content) << " of instance " << instancePublicId;
       throw OrthancException(ErrorCode_InternalError);
     }
 
     if (uncompressIfNeeded)
     {
-      StorageAccessor accessor(area_);
-      accessor.Read(result, attachment);
+      ReadAttachment(result, attachment);
     }
     else
     {
@@ -407,10 +612,19 @@
   }
 
 
+  void ServerContext::ReadAttachment(std::string& result,
+                                     const FileInfo& attachment)
+  {
+    // This will decompress the attachment
+    StorageAccessor accessor(area_);
+    accessor.Read(result, attachment);
+  }
+
+
   IDynamicObject* ServerContext::DicomCacheProvider::Provide(const std::string& instancePublicId)
   {
     std::string content;
-    context_.ReadFile(content, instancePublicId, FileContentType_Dicom);
+    context_.ReadDicom(content, instancePublicId);
     return new ParsedDicomFile(content);
   }
 
@@ -472,6 +686,13 @@
                                      const std::string& uuid,
                                      ResourceType expectedType)
   {
+    if (expectedType == ResourceType_Instance)
+    {
+      // remove the file from the DicomCache
+      boost::mutex::scoped_lock lock(dicomCacheMutex_);
+      dicomCache_.Invalidate(uuid);
+    }
+
     return index_.DeleteResource(target, uuid, expectedType);
   }
 
@@ -482,7 +703,7 @@
   }
 
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
   void ServerContext::SetPlugins(OrthancPlugins& plugins)
   {
     boost::recursive_mutex::scoped_lock lock(listenersMutex_);
@@ -491,7 +712,7 @@
 
     // TODO REFACTOR THIS
     listeners_.clear();
-    listeners_.push_back(ServerListener(lua_, "Lua"));
+    listeners_.push_back(ServerListener(luaListener_, "Lua"));
     listeners_.push_back(ServerListener(plugins, "plugin"));
   }
 
@@ -504,7 +725,7 @@
 
     // TODO REFACTOR THIS
     listeners_.clear();
-    listeners_.push_back(ServerListener(lua_, "Lua"));
+    listeners_.push_back(ServerListener(luaListener_, "Lua"));
   }
 
 
@@ -537,7 +758,7 @@
 
   bool ServerContext::HasPlugins() const
   {
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
     return (plugins_ != NULL);
 #else
     return false;
@@ -545,28 +766,39 @@
   }
 
 
-  bool ServerContext::Apply(std::list<std::string>& result,
+  void ServerContext::Apply(bool& isComplete, 
+                            std::list<std::string>& result,
                             const ::Orthanc::LookupResource& lookup,
-                            size_t maxResults)
+                            size_t since,
+                            size_t limit)
   {
     result.clear();
+    isComplete = true;
 
     std::vector<std::string> resources, instances;
     GetIndex().FindCandidates(resources, instances, lookup);
 
     assert(resources.size() == instances.size());
 
+    size_t skipped = 0;
     for (size_t i = 0; i < instances.size(); i++)
     {
+      // TODO - Don't read the full JSON from the disk if only "main
+      // DICOM tags" are to be returned
       Json::Value dicom;
-      ReadJson(dicom, instances[i]);
+      ReadDicomAsJson(dicom, instances[i]);
       
       if (lookup.IsMatch(dicom))
       {
-        if (maxResults != 0 &&
-            result.size() >= maxResults)
+        if (skipped < since)
         {
-          return false;  // too many results
+          skipped++;
+        }
+        else if (limit != 0 &&
+                 result.size() >= limit)
+        {
+          isComplete = false;
+          return;  // too many results
         }
         else
         {
@@ -574,8 +806,21 @@
         }
       }
     }
-
-    return true;  // finished
   }
 
+
+  void ServerContext::AddChildInstances(SetOfInstancesJob& job,
+                                        const std::string& publicId)
+  {
+    std::list<std::string> instances;
+    GetIndex().GetChildInstances(instances, publicId);
+
+    job.Reserve(job.GetInstancesCount() + instances.size());
+
+    for (std::list<std::string>::const_iterator
+           it = instances.begin(); it != instances.end(); ++it)
+    {
+      job.AddInstance(*it);
+    }
+  }
 }
--- a/OrthancServer/ServerContext.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/ServerContext.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,22 +33,21 @@
 
 #pragma once
 
-#include "../Core/MultiThreading/SharedMessageQueue.h"
+#include "DicomInstanceToStore.h"
+#include "IServerListener.h"
+#include "LuaScripting.h"
+#include "OrthancHttpHandler.h"
+#include "ServerIndex.h"
+
 #include "../Core/Cache/MemoryCache.h"
 #include "../Core/Cache/SharedArchive.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
 #include "../Core/FileStorage/IStorageArea.h"
-#include "../Core/Lua/LuaContext.h"
+#include "../Core/JobsEngine/JobsEngine.h"
+#include "../Core/JobsEngine/SetOfInstancesJob.h"
+#include "../Core/MultiThreading/SharedMessageQueue.h"
 #include "../Core/RestApi/RestApiOutput.h"
 #include "../Plugins/Engine/OrthancPlugins.h"
-#include "DicomInstanceToStore.h"
-#include "DicomProtocol/ReusableDicomUserConnection.h"
-#include "IServerListener.h"
-#include "LuaScripting.h"
-#include "ParsedDicomFile.h"
-#include "Scheduler/ServerScheduler.h"
-#include "ServerIndex.h"
-#include "OrthancHttpHandler.h"
-#include "Search/LookupResource.h"
 
 #include <boost/filesystem.hpp>
 #include <boost/thread.hpp>
@@ -60,9 +60,39 @@
    * filesystem (including compression), as well as the index of the
    * DICOM store. It implements the required locking mechanisms.
    **/
-  class ServerContext
+  class ServerContext : private JobsRegistry::IObserver
   {
   private:
+    class LuaServerListener : public IServerListener
+    {
+    private:
+      ServerContext& context_;
+
+    public:
+      LuaServerListener(ServerContext& context) :
+        context_(context)
+      {
+      }
+
+      virtual void SignalStoredInstance(const std::string& publicId,
+                                        DicomInstanceToStore& instance,
+                                        const Json::Value& simplifiedTags)
+      {
+        context_.mainLua_.SignalStoredInstance(publicId, instance, simplifiedTags);
+      }
+    
+      virtual void SignalChange(const ServerIndexChange& change)
+      {
+        context_.mainLua_.SignalChange(change);
+      }
+
+      virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance,
+                                          const Json::Value& simplified)
+      {
+        return context_.filterLua_.FilterIncomingInstance(instance, simplified);
+      }
+    };
+    
     class DicomCacheProvider : public ICachePageProvider
     {
     private:
@@ -104,8 +134,22 @@
     typedef std::list<ServerListener>  ServerListeners;
 
 
-    static void ChangeThread(ServerContext* that);
+    static void ChangeThread(ServerContext* that,
+                             unsigned int sleepDelay);
+
+    static void SaveJobsThread(ServerContext* that,
+                               unsigned int sleepDelay);
 
+    void ReadDicomAsJsonInternal(std::string& result,
+                                 const std::string& instancePublicId);
+
+    void SaveJobsEngine();
+
+    virtual void SignalJobSubmitted(const std::string& jobId);
+
+    virtual void SignalJobSuccess(const std::string& jobId);
+
+    virtual void SignalJobFailure(const std::string& jobId);
 
     ServerIndex index_;
     IStorageArea& area_;
@@ -116,12 +160,13 @@
     DicomCacheProvider provider_;
     boost::mutex dicomCacheMutex_;
     MemoryCache dicomCache_;
-    ReusableDicomUserConnection scu_;
-    ServerScheduler scheduler_;
+    JobsEngine jobsEngine_;
 
-    LuaScripting lua_;
+    LuaScripting mainLua_;
+    LuaScripting filterLua_;
+    LuaServerListener  luaListener_;
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
     OrthancPlugins* plugins_;
 #endif
 
@@ -129,8 +174,11 @@
     boost::recursive_mutex listenersMutex_;
 
     bool done_;
+    bool haveJobsChanged_;
+    bool isJobsEngineUnserialized_;
     SharedMessageQueue  pendingChanges_;
     boost::thread  changeThread_;
+    boost::thread  saveJobsThread_;
         
     SharedArchive  queryRetrieveArchive_;
     std::string defaultLocalAet_;
@@ -157,10 +205,14 @@
     };
 
     ServerContext(IDatabaseWrapper& database,
-                  IStorageArea& area);
+                  IStorageArea& area,
+                  bool unitTesting);
 
     ~ServerContext();
 
+    void SetupJobsEngine(bool unitTesting,
+                         bool loadJobsFromDatabase);
+
     ServerIndex& GetIndex()
     {
       return index_;
@@ -192,14 +244,42 @@
                                      FileContentType attachmentType,
                                      CompressionType compression);
 
-    void ReadJson(Json::Value& result,
-                  const std::string& instancePublicId);
+    void ReadDicomAsJson(std::string& result,
+                         const std::string& instancePublicId,
+                         const std::set<DicomTag>& ignoreTagLength);
+
+    void ReadDicomAsJson(Json::Value& result,
+                         const std::string& instancePublicId,
+                         const std::set<DicomTag>& ignoreTagLength);
 
+    void ReadDicomAsJson(std::string& result,
+                         const std::string& instancePublicId)
+    {
+      std::set<DicomTag> ignoreTagLength;
+      ReadDicomAsJson(result, instancePublicId, ignoreTagLength);
+    }
+
+    void ReadDicomAsJson(Json::Value& result,
+                         const std::string& instancePublicId)
+    {
+      std::set<DicomTag> ignoreTagLength;
+      ReadDicomAsJson(result, instancePublicId, ignoreTagLength);
+    }
+
+    void ReadDicom(std::string& dicom,
+                   const std::string& instancePublicId)
+    {
+      ReadAttachment(dicom, instancePublicId, FileContentType_Dicom, true);
+    }
+    
     // TODO CACHING MECHANISM AT THIS POINT
-    void ReadFile(std::string& result,
-                  const std::string& instancePublicId,
-                  FileContentType content,
-                  bool uncompressIfNeeded = true);
+    void ReadAttachment(std::string& result,
+                        const std::string& instancePublicId,
+                        FileContentType content,
+                        bool uncompressIfNeeded);
+    
+    void ReadAttachment(std::string& result,
+                        const FileInfo& attachment);
 
     void SetStoreMD5ForAttachments(bool storeMD5);
 
@@ -208,14 +288,9 @@
       return storeMD5_;
     }
 
-    ReusableDicomUserConnection& GetReusableDicomUserConnection()
+    JobsEngine& GetJobsEngine()
     {
-      return scu_;
-    }
-
-    ServerScheduler& GetScheduler()
-    {
-      return scheduler_;
+      return jobsEngine_;
     }
 
     bool DeleteResource(Json::Value& target,
@@ -234,9 +309,9 @@
       return defaultLocalAet_;
     }
 
-    LuaScripting& GetLua()
+    LuaScripting& GetLuaScripting()
     {
-      return lua_;
+      return mainLua_;
     }
 
     OrthancHttpHandler& GetHttpHandler()
@@ -246,16 +321,18 @@
 
     void Stop();
 
-    bool Apply(std::list<std::string>& result,
+    void Apply(bool& isComplete, 
+               std::list<std::string>& result,
                const ::Orthanc::LookupResource& lookup,
-               size_t maxResults);
+               size_t since,
+               size_t limit);
 
 
     /**
      * Management of the plugins
      **/
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
     void SetPlugins(OrthancPlugins& plugins);
 
     void ResetPlugins();
@@ -266,5 +343,8 @@
 #endif
 
     bool HasPlugins() const;
+
+    void AddChildInstances(SetOfInstancesJob& job,
+                           const std::string& publicId);
   };
 }
--- a/OrthancServer/ServerEnumerations.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/ServerEnumerations.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -35,15 +36,19 @@
 
 #include "../Core/OrthancException.h"
 #include "../Core/EnumerationDictionary.h"
+#include "../Core/Logging.h"
 #include "../Core/Toolbox.h"
 
 #include <boost/thread.hpp>
 
 namespace Orthanc
 {
+  typedef std::map<FileContentType, std::string>  MimeTypes;
+
   static boost::mutex enumerationsMutex_;
   static Toolbox::EnumerationDictionary<MetadataType> dictMetadataType_;
   static Toolbox::EnumerationDictionary<FileContentType> dictContentType_;
+  static MimeTypes  mimeTypes_;
 
   void InitializeServerEnumerations()
   {
@@ -59,6 +64,12 @@
     dictMetadataType_.Add(MetadataType_ModifiedFrom, "ModifiedFrom");
     dictMetadataType_.Add(MetadataType_AnonymizedFrom, "AnonymizedFrom");
     dictMetadataType_.Add(MetadataType_LastUpdate, "LastUpdate");
+    dictMetadataType_.Add(MetadataType_Instance_Origin, "Origin");
+    dictMetadataType_.Add(MetadataType_Instance_TransferSyntax, "TransferSyntax");
+    dictMetadataType_.Add(MetadataType_Instance_SopClassUid, "SopClassUid");
+    dictMetadataType_.Add(MetadataType_Instance_RemoteIp, "RemoteIP");
+    dictMetadataType_.Add(MetadataType_Instance_CalledAet, "CalledAET");
+    dictMetadataType_.Add(MetadataType_Instance_HttpUsername, "HttpUsername");
 
     dictContentType_.Add(FileContentType_Dicom, "dicom");
     dictContentType_.Add(FileContentType_DicomAsJson, "dicom-as-json");
@@ -69,13 +80,29 @@
   {
     boost::mutex::scoped_lock lock(enumerationsMutex_);
 
-    if (metadata < static_cast<int>(MetadataType_StartUser) ||
-        metadata > static_cast<int>(MetadataType_EndUser))
+    MetadataType type = static_cast<MetadataType>(metadata);
+
+    if (metadata < 0 || 
+        !IsUserMetadata(type))
     {
+      LOG(ERROR) << "A user content type must have index between "
+                 << static_cast<int>(MetadataType_StartUser) << " and "
+                 << static_cast<int>(MetadataType_EndUser) << ", but \""
+                 << name << "\" has index " << metadata;
+        
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-    dictMetadataType_.Add(static_cast<MetadataType>(metadata), name);
+    if (dictMetadataType_.Contains(type))
+    {
+      LOG(ERROR) << "Cannot associate user content type \""
+                 << name << "\" with index " << metadata 
+                 << ", as this index is already used";
+        
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    dictMetadataType_.Add(type, name);
   }
 
   std::string EnumerationToString(MetadataType type)
@@ -93,17 +120,35 @@
   }
 
   void RegisterUserContentType(int contentType,
-                               const std::string& name)
+                               const std::string& name,
+                               const std::string& mime)
   {
     boost::mutex::scoped_lock lock(enumerationsMutex_);
 
-    if (contentType < static_cast<int>(FileContentType_StartUser) ||
-        contentType > static_cast<int>(FileContentType_EndUser))
+    FileContentType type = static_cast<FileContentType>(contentType);
+
+    if (contentType < 0 || 
+        !IsUserContentType(type))
     {
+      LOG(ERROR) << "A user content type must have index between "
+                 << static_cast<int>(FileContentType_StartUser) << " and "
+                 << static_cast<int>(FileContentType_EndUser) << ", but \""
+                 << name << "\" has index " << contentType;
+        
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-    dictContentType_.Add(static_cast<FileContentType>(contentType), name);
+    if (dictContentType_.Contains(type))
+    {
+      LOG(ERROR) << "Cannot associate user content type \""
+                 << name << "\" with index " << contentType 
+                 << ", as this index is already used";
+        
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    dictContentType_.Add(type, name);
+    mimeTypes_[type] = mime;
   }
 
   std::string EnumerationToString(FileContentType type)
@@ -114,6 +159,33 @@
     return dictContentType_.Translate(type);
   }
 
+  std::string GetFileContentMime(FileContentType type)
+  {
+    if (type >= FileContentType_StartUser &&
+        type <= FileContentType_EndUser)
+    {
+      boost::mutex::scoped_lock lock(enumerationsMutex_);
+      
+      MimeTypes::const_iterator it = mimeTypes_.find(type);
+      if (it != mimeTypes_.end())
+      {
+        return it->second;
+      }
+    }
+
+    switch (type)
+    {
+      case FileContentType_Dicom:
+        return "application/dicom";
+
+      case FileContentType_DicomAsJson:
+        return "application/json";
+
+      default:
+        return "application/octet-stream";
+    }
+  }
+
   FileContentType StringToContentType(const std::string& str)
   {
     boost::mutex::scoped_lock lock(enumerationsMutex_);
@@ -237,131 +309,21 @@
       case ChangeType_NewChildInstance:
         return "NewChildInstance";
 
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  const char* EnumerationToString(ModalityManufacturer manufacturer)
-  {
-    switch (manufacturer)
-    {
-      case ModalityManufacturer_Generic:
-        return "Generic";
+      case ChangeType_UpdatedAttachment:
+        return "UpdatedAttachment";
 
-      case ModalityManufacturer_StoreScp:
-        return "StoreScp";
-      
-      case ModalityManufacturer_ClearCanvas:
-        return "ClearCanvas";
-      
-      case ModalityManufacturer_MedInria:
-        return "MedInria";
+      case ChangeType_UpdatedMetadata:
+        return "UpdatedMetadata";
 
-      case ModalityManufacturer_Dcm4Chee:
-        return "Dcm4Chee";
-      
-      case ModalityManufacturer_SyngoVia:
-        return "SyngoVia";
-      
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
 
-
-  const char* EnumerationToString(DicomRequestType type)
-  {
-    switch (type)
-    {
-      case DicomRequestType_Echo:
-        return "Echo";
-        break;
-
-      case DicomRequestType_Find:
-        return "Find";
-        break;
-
-      case DicomRequestType_Get:
-        return "Get";
-        break;
-
-      case DicomRequestType_Move:
-        return "Move";
-        break;
-
-      case DicomRequestType_Store:
-        return "Store";
-        break;
-
-      default: 
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-
-  ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer)
+  
+  bool IsUserMetadata(MetadataType metadata)
   {
-    if (manufacturer == "Generic")
-    {
-      return ModalityManufacturer_Generic;
-    }
-    else if (manufacturer == "ClearCanvas")
-    {
-      return ModalityManufacturer_ClearCanvas;
-    }
-    else if (manufacturer == "StoreScp")
-    {
-      return ModalityManufacturer_StoreScp;
-    }
-    else if (manufacturer == "MedInria")
-    {
-      return ModalityManufacturer_MedInria;
-    }
-    else if (manufacturer == "Dcm4Chee")
-    {
-      return ModalityManufacturer_Dcm4Chee;
-    }
-    else if (manufacturer == "SyngoVia")
-    {
-      return ModalityManufacturer_SyngoVia;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  const char* EnumerationToString(TransferSyntax syntax)
-  {
-    switch (syntax)
-    {
-      case TransferSyntax_Deflated:
-        return "Deflated";
-
-      case TransferSyntax_Jpeg:
-        return "JPEG";
-
-      case TransferSyntax_Jpeg2000:
-        return "JPEG2000";
-
-      case TransferSyntax_JpegLossless:
-        return "JPEG Lossless";
-
-      case TransferSyntax_Jpip:
-        return "JPIP";
-
-      case TransferSyntax_Mpeg2:
-        return "MPEG2";
-
-      case TransferSyntax_Rle:
-        return "RLE";
-
-      default: 
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
+    return (metadata >= MetadataType_StartUser &&
+            metadata <= MetadataType_EndUser);
   }
 }
--- a/OrthancServer/ServerEnumerations.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/ServerEnumerations.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -55,76 +56,6 @@
     StoreStatus_FilteredOut     // Removed by NewInstanceFilter
   };
 
-  enum ModalityManufacturer
-  {
-    ModalityManufacturer_Generic,
-    ModalityManufacturer_StoreScp,
-    ModalityManufacturer_ClearCanvas,
-    ModalityManufacturer_MedInria,
-    ModalityManufacturer_Dcm4Chee,
-    ModalityManufacturer_SyngoVia
-  };
-
-  enum DicomRequestType
-  {
-    DicomRequestType_Echo,
-    DicomRequestType_Find,
-    DicomRequestType_Get,
-    DicomRequestType_Move,
-    DicomRequestType_Store
-  };
-
-  enum DicomReplaceMode
-  {
-    DicomReplaceMode_InsertIfAbsent,
-    DicomReplaceMode_ThrowIfAbsent,
-    DicomReplaceMode_IgnoreIfAbsent
-  };
-
-  enum TransferSyntax
-  {
-    TransferSyntax_Deflated,
-    TransferSyntax_Jpeg,
-    TransferSyntax_Jpeg2000,
-    TransferSyntax_JpegLossless,
-    TransferSyntax_Jpip,
-    TransferSyntax_Mpeg2,
-    TransferSyntax_Rle
-  };
-
-  enum ValueRepresentation
-  {
-    ValueRepresentation_Other,
-    ValueRepresentation_PatientName,
-    ValueRepresentation_Date,
-    ValueRepresentation_DateTime,
-    ValueRepresentation_Time
-  };
-
-  enum DicomToJsonFormat
-  {
-    DicomToJsonFormat_Full,
-    DicomToJsonFormat_Short,
-    DicomToJsonFormat_Simple
-  };
-
-  enum DicomToJsonFlags
-  {
-    DicomToJsonFlags_IncludeBinary         = (1 << 0),
-    DicomToJsonFlags_IncludePrivateTags    = (1 << 1),
-    DicomToJsonFlags_IncludeUnknownTags    = (1 << 2),
-    DicomToJsonFlags_IncludePixelData      = (1 << 3),
-    DicomToJsonFlags_ConvertBinaryToAscii  = (1 << 4),
-    DicomToJsonFlags_ConvertBinaryToNull   = (1 << 5),
-
-    // Some predefined combinations
-    DicomToJsonFlags_None     = 0,
-    DicomToJsonFlags_Default  = (DicomToJsonFlags_IncludePrivateTags | 
-                                 DicomToJsonFlags_IncludeUnknownTags | 
-                                 DicomToJsonFlags_IncludePixelData | 
-                                 DicomToJsonFlags_ConvertBinaryToNull)
-  };
-
   enum IdentifierConstraintType
   {
     IdentifierConstraintType_Equal,
@@ -144,7 +75,23 @@
   {
     GlobalProperty_DatabaseSchemaVersion = 1,   // Unused in the Orthanc core as of Orthanc 0.9.5
     GlobalProperty_FlushSleep = 2,
-    GlobalProperty_AnonymizationSequence = 3
+    GlobalProperty_AnonymizationSequence = 3,
+    GlobalProperty_JobsRegistry = 5,
+    GlobalProperty_TotalCompressedSize = 6,     // Reserved for Orthanc > 1.4.1
+    GlobalProperty_TotalUncompressedSize = 7,   // Reserved for Orthanc > 1.4.1
+
+    // Reserved values for internal use by the database plugins
+    GlobalProperty_DatabasePatchLevel = 4,
+    GlobalProperty_DatabaseInternal0 = 10,
+    GlobalProperty_DatabaseInternal1 = 11,
+    GlobalProperty_DatabaseInternal2 = 12,
+    GlobalProperty_DatabaseInternal3 = 13,
+    GlobalProperty_DatabaseInternal4 = 14,
+    GlobalProperty_DatabaseInternal5 = 15,
+    GlobalProperty_DatabaseInternal6 = 16,
+    GlobalProperty_DatabaseInternal7 = 17,
+    GlobalProperty_DatabaseInternal8 = 18,
+    GlobalProperty_DatabaseInternal9 = 19
   };
 
   enum MetadataType
@@ -156,6 +103,12 @@
     MetadataType_ModifiedFrom = 5,
     MetadataType_AnonymizedFrom = 6,
     MetadataType_LastUpdate = 7,
+    MetadataType_Instance_Origin = 8,          // New in Orthanc 0.9.5
+    MetadataType_Instance_TransferSyntax = 9,  // New in Orthanc 1.2.0
+    MetadataType_Instance_SopClassUid = 10,    // New in Orthanc 1.2.0
+    MetadataType_Instance_RemoteIp = 11,       // New in Orthanc 1.4.0
+    MetadataType_Instance_CalledAet = 12,      // New in Orthanc 1.4.0
+    MetadataType_Instance_HttpUsername = 13,   // New in Orthanc 1.4.0
 
     // Make sure that the value "65535" can be stored into this enumeration
     MetadataType_StartUser = 1024,
@@ -178,6 +131,8 @@
     ChangeType_StablePatient = 12,
     ChangeType_StableStudy = 13,
     ChangeType_StableSeries = 14,
+    ChangeType_UpdatedAttachment = 15,
+    ChangeType_UpdatedMetadata = 16,
 
     ChangeType_INTERNAL_LastLogged = 4095,
 
@@ -198,12 +153,15 @@
   std::string EnumerationToString(MetadataType type);
 
   void RegisterUserContentType(int contentType,
-                               const std::string& name);
+                               const std::string& name,
+                               const std::string& mime);
 
   FileContentType StringToContentType(const std::string& str);
 
   std::string EnumerationToString(FileContentType type);
 
+  std::string GetFileContentMime(FileContentType type);
+
   std::string GetBasePath(ResourceType type,
                           const std::string& publicId);
 
@@ -213,11 +171,5 @@
 
   const char* EnumerationToString(ChangeType type);
 
-  const char* EnumerationToString(ModalityManufacturer manufacturer);
-
-  const char* EnumerationToString(DicomRequestType type);
-
-  const char* EnumerationToString(TransferSyntax syntax);
-
-  ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer);
+  bool IsUserMetadata(MetadataType type);
 }
--- a/OrthancServer/ServerIndex.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/ServerIndex.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -40,17 +41,17 @@
 #include "ServerIndexChange.h"
 #include "EmbeddedResources.h"
 #include "OrthancInitialization.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
 #include "ServerToolbox.h"
 #include "../Core/Toolbox.h"
 #include "../Core/Logging.h"
-#include "../Core/Uuid.h"
 #include "../Core/DicomFormat/DicomArray.h"
-#include "Search/LookupIdentifierQuery.h"
+
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "ServerContext.h"
+#include "DicomInstanceToStore.h"
 #include "Search/LookupResource.h"
 
-#include "FromDcmtkBridge.h"
-#include "ServerContext.h"
-
 #include <boost/lexical_cast.hpp>
 #include <stdio.h>
 
@@ -341,7 +342,8 @@
   }
 
 
-  void ServerIndex::FlushThread(ServerIndex* that)
+  void ServerIndex::FlushThread(ServerIndex* that,
+                                unsigned int threadSleep)
   {
     // By default, wait for 10 seconds before flushing
     unsigned int sleep = 10;
@@ -367,13 +369,15 @@
 
     while (!that->done_)
     {
-      boost::this_thread::sleep(boost::posix_time::seconds(1));
+      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep));
       count++;
       if (count < sleep)
       {
         continue;
       }
 
+      Logging::Flush();
+
       boost::mutex::scoped_lock lock(that->mutex_);
       that->db_.FlushToDisk();
       count = 0;
@@ -535,11 +539,13 @@
 
 
   ServerIndex::ServerIndex(ServerContext& context,
-                           IDatabaseWrapper& db) : 
+                           IDatabaseWrapper& db,
+                           unsigned int threadSleep) : 
     done_(false),
     db_(db),
     maximumStorageSize_(0),
-    maximumPatients_(0)
+    maximumPatients_(0),
+    overwrite_(false)
   {
     listener_.reset(new Listener(context));
     db_.SetListener(*listener_);
@@ -552,10 +558,11 @@
 
     if (db.HasFlushToDisk())
     {
-      flushThread_ = boost::thread(FlushThread, this);
+      flushThread_ = boost::thread(FlushThread, this, threadSleep);
     }
 
-    unstableResourcesMonitorThread_ = boost::thread(UnstableResourcesMonitorThread, this);
+    unstableResourcesMonitorThread_ = boost::thread
+      (UnstableResourcesMonitorThread, this, threadSleep);
   }
 
 
@@ -592,31 +599,54 @@
 
 
 
+  void ServerIndex::SetInstanceMetadata(std::map<MetadataType, std::string>& instanceMetadata,
+                                        int64_t instance,
+                                        MetadataType metadata,
+                                        const std::string& value)
+  {
+    db_.SetMetadata(instance, metadata, value);
+    instanceMetadata[metadata] = value;
+  }
+
+
+
   StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata,
-                                 const DicomMap& dicomSummary,
-                                 const Attachments& attachments,
-                                 const std::string& remoteAet,
-                                 const MetadataMap& metadata)
+                                 DicomInstanceToStore& instanceToStore,
+                                 const Attachments& attachments)
   {
     boost::mutex::scoped_lock lock(mutex_);
 
+    const DicomMap& dicomSummary = instanceToStore.GetSummary();
+    const ServerIndex::MetadataMap& metadata = instanceToStore.GetMetadata();
+
     instanceMetadata.clear();
 
-    DicomInstanceHasher hasher(dicomSummary);
+    DicomInstanceHasher hasher(instanceToStore.GetSummary());
 
     try
     {
       Transaction t(*this);
 
-      // Do nothing if the instance already exists
+      // Check whether this instance is already stored
       {
         ResourceType type;
         int64_t tmp;
         if (db_.LookupResource(tmp, type, hasher.HashInstance()))
         {
           assert(type == ResourceType_Instance);
-          db_.GetAllMetadata(instanceMetadata, tmp);
-          return StoreStatus_AlreadyStored;
+
+          if (overwrite_)
+          {
+            // Overwrite the old instance
+            LOG(INFO) << "Overwriting instance: " << hasher.HashInstance();
+            db_.DeleteResource(tmp);
+          }
+          else
+          {
+            // Do nothing if the instance already exists
+            db_.GetAllMetadata(instanceMetadata, tmp);
+            return StoreStatus_AlreadyStored;
+          }
         }
       }
 
@@ -632,7 +662,7 @@
 
       // Create the instance
       int64_t instance = CreateResource(hasher.HashInstance(), ResourceType_Instance);
-      Toolbox::SetMainDicomTags(db_, instance, ResourceType_Instance, dicomSummary);
+      ServerToolbox::StoreMainDicomTags(db_, instance, ResourceType_Instance, dicomSummary);
 
       // Detect up to which level the patient/study/series/instance
       // hierarchy must be created
@@ -684,21 +714,21 @@
       if (isNewSeries)
       {
         series = CreateResource(hasher.HashSeries(), ResourceType_Series);
-        Toolbox::SetMainDicomTags(db_, series, ResourceType_Series, dicomSummary);
+        ServerToolbox::StoreMainDicomTags(db_, series, ResourceType_Series, dicomSummary);
       }
 
       // Create the study if needed
       if (isNewStudy)
       {
         study = CreateResource(hasher.HashStudy(), ResourceType_Study);
-        Toolbox::SetMainDicomTags(db_, study, ResourceType_Study, dicomSummary);
+        ServerToolbox::StoreMainDicomTags(db_, study, ResourceType_Study, dicomSummary);
       }
 
       // Create the patient if needed
       if (isNewPatient)
       {
         patient = CreateResource(hasher.HashPatient(), ResourceType_Patient);
-        Toolbox::SetMainDicomTags(db_, patient, ResourceType_Patient, dicomSummary);
+        ServerToolbox::StoreMainDicomTags(db_, patient, ResourceType_Patient, dicomSummary);
       }
 
       // Create the parent-to-child links
@@ -746,8 +776,7 @@
             break;
 
           case ResourceType_Instance:
-            db_.SetMetadata(instance, it->first.second, it->second);
-            instanceMetadata[it->first.second] = it->second;
+            SetInstanceMetadata(instanceMetadata, instance, it->first.second, it->second);
             break;
 
           default:
@@ -756,28 +785,62 @@
       }
 
       // Attach the auto-computed metadata for the patient/study/series levels
-      std::string now = Toolbox::GetNowIsoString();
+      std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */);
       db_.SetMetadata(series, MetadataType_LastUpdate, now);
       db_.SetMetadata(study, MetadataType_LastUpdate, now);
       db_.SetMetadata(patient, MetadataType_LastUpdate, now);
 
       // Attach the auto-computed metadata for the instance level,
       // reflecting these additions into the input metadata map
-      db_.SetMetadata(instance, MetadataType_Instance_ReceptionDate, now);
-      instanceMetadata[MetadataType_Instance_ReceptionDate] = now;
-
-      db_.SetMetadata(instance, MetadataType_Instance_RemoteAet, remoteAet);
-      instanceMetadata[MetadataType_Instance_RemoteAet] = remoteAet;
+      SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_ReceptionDate, now);
+      SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteAet,
+                          instanceToStore.GetOrigin().GetRemoteAetC());
+      SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_Origin, 
+                          EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin()));
+
+      {
+        std::string s;
+
+        if (instanceToStore.LookupTransferSyntax(s))
+        {
+          // New in Orthanc 1.2.0
+          SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_TransferSyntax, s);
+        }
+
+        if (instanceToStore.GetOrigin().LookupRemoteIp(s))
+        {
+          // New in Orthanc 1.4.0
+          SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteIp, s);
+        }
+
+        if (instanceToStore.GetOrigin().LookupCalledAet(s))
+        {
+          // New in Orthanc 1.4.0
+          SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_CalledAet, s);
+        }
+
+        if (instanceToStore.GetOrigin().LookupHttpUsername(s))
+        {
+          // New in Orthanc 1.4.0
+          SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_HttpUsername, s);
+        }
+      }
 
       const DicomValue* value;
+      if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
+          !value->IsNull() &&
+          !value->IsBinary())
+      {
+        SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_SopClassUid, value->GetContent());
+      }
+
       if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL ||
           (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL)
       {
         if (!value->IsNull() && 
             !value->IsBinary())
         {
-          db_.SetMetadata(instance, MetadataType_Instance_IndexInSeries, value->GetContent());
-          instanceMetadata[MetadataType_Instance_IndexInSeries] = value->GetContent();
+          SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_IndexInSeries, value->GetContent());
         }
       }
 
@@ -1155,7 +1218,12 @@
 
     {
       boost::mutex::scoped_lock lock(mutex_);
+
+      // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as
+      // "GetLastChange()" involves calls to "GetPublicId()"
+      Transaction transaction(*this);
       db_.GetChanges(changes, done, since, maxResults);
+      transaction.Commit(0);
     }
 
     FormatLog(target, changes, "Changes", done, since);
@@ -1168,7 +1236,12 @@
 
     {
       boost::mutex::scoped_lock lock(mutex_);
+
+      // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as
+      // "GetLastChange()" involves calls to "GetPublicId()"
+      Transaction transaction(*this);
       db_.GetLastChange(changes);
+      transaction.Commit(0);
     }
 
     FormatLog(target, changes, "Changes", true, 0);
@@ -1185,7 +1258,7 @@
     ResourceType type;
     if (!db_.LookupResource(id, type, publicId))
     {
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InexistentItem);
     }
 
     std::string patientId;
@@ -1206,22 +1279,34 @@
       switch (currentType)
       {
         case ResourceType_Patient:
-          patientId = map.GetValue(DICOM_TAG_PATIENT_ID).GetContent();
+          if (map.HasTag(DICOM_TAG_PATIENT_ID))
+          {
+            patientId = map.GetValue(DICOM_TAG_PATIENT_ID).GetContent();
+          }
           done = true;
           break;
 
         case ResourceType_Study:
-          studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent();
+          if (map.HasTag(DICOM_TAG_STUDY_INSTANCE_UID))
+          {
+            studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent();
+          }
           currentType = ResourceType_Patient;
           break;
 
         case ResourceType_Series:
-          seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent();
+          if (map.HasTag(DICOM_TAG_SERIES_INSTANCE_UID))
+          {
+            seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent();
+          }
           currentType = ResourceType_Study;
           break;
 
         case ResourceType_Instance:
-          sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent();
+          if (map.HasTag(DICOM_TAG_SOP_INSTANCE_UID))
+          {
+            sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent();
+          }
           currentType = ResourceType_Series;
           break;
 
@@ -1242,7 +1327,7 @@
                               type,
                               publicId,
                               remoteModality,
-                              Toolbox::GetNowIsoString(),
+                              SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */),
                               patientId,
                               studyInstanceUid,
                               seriesInstanceUid,
@@ -1388,6 +1473,13 @@
     StandaloneRecycling();
   }
 
+  void ServerIndex::SetOverwriteInstances(bool overwrite)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    overwrite_ = overwrite;
+  }
+
+
   void ServerIndex::StandaloneRecycling()
   {
     // WARNING: No mutex here, do not include this as a public method
@@ -1525,6 +1617,7 @@
                                 const std::string& value)
   {
     boost::mutex::scoped_lock lock(mutex_);
+    Transaction t(*this);
 
     ResourceType rtype;
     int64_t id;
@@ -1534,6 +1627,13 @@
     }
 
     db_.SetMetadata(id, type, value);
+
+    if (IsUserMetadata(type))
+    {
+      LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId);
+    }
+
+    t.Commit(0);
   }
 
 
@@ -1541,6 +1641,7 @@
                                    MetadataType type)
   {
     boost::mutex::scoped_lock lock(mutex_);
+    Transaction t(*this);
 
     ResourceType rtype;
     int64_t id;
@@ -1550,6 +1651,13 @@
     }
 
     db_.DeleteMetadata(id, type);
+
+    if (IsUserMetadata(type))
+    {
+      LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId);
+    }
+
+    t.Commit(0);
   }
 
 
@@ -1822,9 +1930,10 @@
   }
 
 
-  void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that)
+  void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that,
+                                                   unsigned int threadSleep)
   {
-    int stableAge = Configuration::GetGlobalIntegerParameter("StableAge", 60);
+    int stableAge = Configuration::GetGlobalUnsignedIntegerParameter("StableAge", 60);
     if (stableAge <= 0)
     {
       stableAge = 60;
@@ -1834,8 +1943,8 @@
 
     while (!that->done_)
     {
-      // Check for stable resources each second
-      boost::this_thread::sleep(boost::posix_time::seconds(1));
+      // Check for stable resources each few seconds
+      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep));
 
       boost::mutex::scoped_lock lock(that->mutex_);
 
@@ -1958,6 +2067,11 @@
 
     db_.AddAttachment(resourceId, attachment);
 
+    if (IsUserContentType(attachment.GetContentType()))
+    {
+      LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId);
+    }
+
     t.Commit(attachment.GetCompressedSize());
 
     return StoreStatus_Success;
@@ -1979,6 +2093,11 @@
 
     db_.DeleteAttachment(id, type);
 
+    if (IsUserContentType(type))
+    {
+      LogChange(id, ChangeType_UpdatedAttachment, rtype, publicId);
+    }
+
     t.Commit(0);
   }
 
@@ -2026,13 +2145,20 @@
   }
 
 
+  bool ServerIndex::LookupGlobalProperty(std::string& value,
+                                         GlobalProperty property)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return db_.LookupGlobalProperty(value, property);
+  }
+  
+
   std::string ServerIndex::GetGlobalProperty(GlobalProperty property,
                                              const std::string& defaultValue)
   {
-    boost::mutex::scoped_lock lock(mutex_);
-
     std::string value;
-    if (db_.LookupGlobalProperty(value, property))
+
+    if (LookupGlobalProperty(value, property))
     {
       return value;
     }
@@ -2134,7 +2260,7 @@
       assert(db_.GetResourceType(*it) == lookup.GetLevel());
       
       int64_t instance;
-      if (!Toolbox::FindOneChildInstance(instance, db_, *it, lookup.GetLevel()))
+      if (!ServerToolbox::FindOneChildInstance(instance, db_, *it, lookup.GetLevel()))
       {
         throw OrthancException(ErrorCode_InternalError);
       }
@@ -2143,4 +2269,99 @@
       instances[pos] = db_.GetPublicId(instance);
     }
   }
+
+
+  bool ServerIndex::LookupParent(std::string& target,
+                                 const std::string& publicId,
+                                 ResourceType parentType)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType type;
+    int64_t id;
+    if (!db_.LookupResource(id, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    while (type != parentType)
+    {
+      int64_t parentId;
+
+      if (type == ResourceType_Patient ||    // Cannot further go up in hierarchy
+          !db_.LookupParent(parentId, id))
+      {
+        return false;
+      }
+
+      id = parentId;
+      type = GetParentResourceType(type);
+    }
+
+    target = db_.GetPublicId(id);
+    return true;
+  }
+
+
+  void ServerIndex::ReconstructInstance(ParsedDicomFile& dicom)
+  {
+    DicomMap summary;
+    dicom.ExtractDicomSummary(summary);
+
+    DicomInstanceHasher hasher(summary);
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    try
+    {
+      Transaction t(*this);
+
+      int64_t patient = -1, study = -1, series = -1, instance = -1;
+
+      ResourceType dummy;      
+      if (!db_.LookupResource(patient, dummy, hasher.HashPatient()) ||
+          !db_.LookupResource(study, dummy, hasher.HashStudy()) ||
+          !db_.LookupResource(series, dummy, hasher.HashSeries()) ||
+          !db_.LookupResource(instance, dummy, hasher.HashInstance()) ||
+          patient == -1 ||
+          study == -1 ||
+          series == -1 ||
+          instance == -1)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      db_.ClearMainDicomTags(patient);
+      db_.ClearMainDicomTags(study);
+      db_.ClearMainDicomTags(series);
+      db_.ClearMainDicomTags(instance);
+
+      ServerToolbox::StoreMainDicomTags(db_, patient, ResourceType_Patient, summary);
+      ServerToolbox::StoreMainDicomTags(db_, study, ResourceType_Study, summary);
+      ServerToolbox::StoreMainDicomTags(db_, series, ResourceType_Series, summary);
+      ServerToolbox::StoreMainDicomTags(db_, instance, ResourceType_Instance, summary);
+
+      {
+        std::string s;
+        if (dicom.LookupTransferSyntax(s))
+        {
+          db_.SetMetadata(instance, MetadataType_Instance_TransferSyntax, s);
+        }
+      }
+
+      const DicomValue* value;
+      if ((value = summary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
+          !value->IsNull() &&
+          !value->IsBinary())
+      {
+        db_.SetMetadata(instance, MetadataType_Instance_SopClassUid, value->GetContent());
+      }
+
+      t.Commit(0);  // No change in the DB size
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
+    }
+  }
 }
--- a/OrthancServer/ServerIndex.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/ServerIndex.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -42,11 +43,12 @@
 
 #include "IDatabaseWrapper.h"
 
-
 namespace Orthanc
 {
   class LookupResource;
   class ServerContext;
+  class DicomInstanceToStore;
+  class ParsedDicomFile;
 
   class ServerIndex : public boost::noncopyable
   {
@@ -68,13 +70,16 @@
     IDatabaseWrapper& db_;
     LeastRecentlyUsedIndex<int64_t, UnstableResourcePayload>  unstableResources_;
 
-    uint64_t currentStorageSize_;
-    uint64_t maximumStorageSize_;
+    uint64_t     currentStorageSize_;
+    uint64_t     maximumStorageSize_;
     unsigned int maximumPatients_;
+    bool         overwrite_;
 
-    static void FlushThread(ServerIndex* that);
+    static void FlushThread(ServerIndex* that,
+                            unsigned int threadSleep);
 
-    static void UnstableResourcesMonitorThread(ServerIndex* that);
+    static void UnstableResourcesMonitorThread(ServerIndex* that,
+                                               unsigned int threadSleep);
 
     void MainDicomTagsToJson(Json::Value& result,
                              int64_t resourceId,
@@ -115,9 +120,15 @@
     int64_t CreateResource(const std::string& publicId,
                            ResourceType type);
 
+    void SetInstanceMetadata(std::map<MetadataType, std::string>& instanceMetadata,
+                             int64_t instance,
+                             MetadataType metadata,
+                             const std::string& value);
+
   public:
     ServerIndex(ServerContext& context,
-                IDatabaseWrapper& database);
+                IDatabaseWrapper& database,
+                unsigned int threadSleep);
 
     ~ServerIndex();
 
@@ -139,11 +150,11 @@
     // "count == 0" means no limit on the number of patients
     void SetMaximumPatientCount(unsigned int count);
 
+    void SetOverwriteInstances(bool overwrite);
+
     StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata,
-                      const DicomMap& dicomSummary,
-                      const Attachments& attachments,
-                      const std::string& remoteAet,
-                      const MetadataMap& metadata);
+                      DicomInstanceToStore& instance,
+                      const Attachments& attachments);
 
     void ComputeStatistics(Json::Value& target);                        
 
@@ -250,6 +261,9 @@
     void SetGlobalProperty(GlobalProperty property,
                            const std::string& value);
 
+    bool LookupGlobalProperty(std::string& value,
+                              GlobalProperty property);
+
     std::string GetGlobalProperty(GlobalProperty property,
                                   const std::string& defaultValue);
 
@@ -266,5 +280,11 @@
     void FindCandidates(std::vector<std::string>& resources,
                         std::vector<std::string>& instances,
                         const ::Orthanc::LookupResource& lookup);
+
+    bool LookupParent(std::string& target,
+                      const std::string& publicId,
+                      ResourceType parentType);
+
+    void ReconstructInstance(ParsedDicomFile& dicom);
   };
 }
--- a/OrthancServer/ServerIndexChange.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/ServerIndexChange.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,7 +35,7 @@
 
 #include "ServerEnumerations.h"
 #include "../Core/IDynamicObject.h"
-#include "../Core/Toolbox.h"
+#include "../Core/SystemToolbox.h"
 
 #include <string>
 #include <json/value.h>
@@ -58,7 +59,7 @@
       changeType_(changeType),
       resourceType_(resourceType),
       publicId_(publicId),
-      date_(Toolbox::GetNowIsoString())
+      date_(SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */))
     {
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/ArchiveJob.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,904 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "ArchiveJob.h"
+
+#include "../../Core/Compression/HierarchicalZipWriter.h"
+#include "../../Core/DicomParsing/DicomDirWriter.h"
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+
+#include <stdio.h>
+
+#if defined(_MSC_VER)
+#define snprintf _snprintf
+#endif
+
+static const uint64_t MEGA_BYTES = 1024 * 1024;
+static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024;
+static const char* MEDIA_IMAGES_FOLDER = "IMAGES"; 
+
+namespace Orthanc
+{
+  static bool IsZip64Required(uint64_t uncompressedSize,
+                              unsigned int countInstances)
+  {
+    static const uint64_t      SAFETY_MARGIN = 64 * MEGA_BYTES;  // Should be large enough to hold DICOMDIR
+    static const unsigned int  FILES_MARGIN = 10;
+
+    /**
+     * Determine whether ZIP64 is required. Original ZIP format can
+     * store up to 2GB of data (some implementation supporting up to
+     * 4GB of data), and up to 65535 files.
+     * https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64
+     **/
+
+    const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES - SAFETY_MARGIN ||
+                          countInstances >= 65535 - FILES_MARGIN);
+
+    LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size "
+              << (uncompressedSize / MEGA_BYTES) << "MB using the "
+              << (isZip64 ? "ZIP64" : "ZIP32") << " file format";
+
+    return isZip64;
+  }
+
+
+  class ArchiveJob::ResourceIdentifiers : public boost::noncopyable
+  {
+  private:
+    ResourceType   level_;
+    std::string    patient_;
+    std::string    study_;
+    std::string    series_;
+    std::string    instance_;
+
+    static void GoToParent(ServerIndex& index,
+                           std::string& current)
+    {
+      std::string tmp;
+
+      if (index.LookupParent(tmp, current))
+      {
+        current = tmp;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+    }
+
+
+  public:
+    ResourceIdentifiers(ServerIndex& index,
+                        const std::string& publicId)
+    {
+      if (!index.LookupResourceType(level_, publicId))
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+
+      std::string current = publicId;;
+      switch (level_)  // Do not add "break" below!
+      {
+        case ResourceType_Instance:
+          instance_ = current;
+          GoToParent(index, current);
+            
+        case ResourceType_Series:
+          series_ = current;
+          GoToParent(index, current);
+
+        case ResourceType_Study:
+          study_ = current;
+          GoToParent(index, current);
+
+        case ResourceType_Patient:
+          patient_ = current;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    const std::string& GetIdentifier(ResourceType level) const
+    {
+      // Some sanity check to ensure enumerations are not altered
+      assert(ResourceType_Patient < ResourceType_Study);
+      assert(ResourceType_Study < ResourceType_Series);
+      assert(ResourceType_Series < ResourceType_Instance);
+
+      if (level > level_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      switch (level)
+      {
+        case ResourceType_Patient:
+          return patient_;
+
+        case ResourceType_Study:
+          return study_;
+
+        case ResourceType_Series:
+          return series_;
+
+        case ResourceType_Instance:
+          return instance_;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  };
+
+
+  class ArchiveJob::IArchiveVisitor : public boost::noncopyable
+  {
+  public:
+    virtual ~IArchiveVisitor()
+    {
+    }
+
+    virtual void Open(ResourceType level,
+                      const std::string& publicId) = 0;
+
+    virtual void Close() = 0;
+
+    virtual void AddInstance(const std::string& instanceId,
+                             const FileInfo& dicom) = 0;
+  };
+
+
+  class ArchiveJob::ArchiveIndex : public boost::noncopyable
+  {
+  private:
+    struct Instance
+    {
+      std::string  id_;
+      FileInfo     dicom_;
+
+      Instance(const std::string& id,
+               const FileInfo& dicom) : 
+        id_(id), dicom_(dicom)
+      {
+      }
+    };
+
+    // A "NULL" value for ArchiveIndex indicates a non-expanded node
+    typedef std::map<std::string, ArchiveIndex*>   Resources;
+
+    ResourceType         level_;
+    Resources            resources_;   // Only at patient/study/series level
+    std::list<Instance>  instances_;   // Only at instance level
+
+
+    void AddResourceToExpand(ServerIndex& index,
+                             const std::string& id)
+    {
+      if (level_ == ResourceType_Instance)
+      {
+        FileInfo tmp;
+        if (index.LookupAttachment(tmp, id, FileContentType_Dicom))
+        {
+          instances_.push_back(Instance(id, tmp));
+        }
+      }
+      else
+      {
+        resources_[id] = NULL;
+      }
+    }
+
+
+  public:
+    ArchiveIndex(ResourceType level) :
+      level_(level)
+    {
+    }
+
+    ~ArchiveIndex()
+    {
+      for (Resources::iterator it = resources_.begin();
+           it != resources_.end(); ++it)
+      {
+        delete it->second;
+      }
+    }
+
+
+    void Add(ServerIndex& index,
+             const ResourceIdentifiers& resource)
+    {
+      const std::string& id = resource.GetIdentifier(level_);
+      Resources::iterator previous = resources_.find(id);
+
+      if (level_ == ResourceType_Instance)
+      {
+        AddResourceToExpand(index, id);
+      }
+      else if (resource.GetLevel() == level_)
+      {
+        // Mark this resource for further expansion
+        if (previous != resources_.end())
+        {
+          delete previous->second;
+        }
+
+        resources_[id] = NULL;
+      }
+      else if (previous == resources_.end())
+      {
+        // This is the first time we meet this resource
+        std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
+        child->Add(index, resource);
+        resources_[id] = child.release();
+      }
+      else if (previous->second != NULL)
+      {
+        previous->second->Add(index, resource);
+      }
+      else
+      {
+        // Nothing to do: This item is marked for further expansion
+      }
+    }
+
+
+    void Expand(ServerIndex& index)
+    {
+      if (level_ == ResourceType_Instance)
+      {
+        // Expanding an instance node makes no sense
+        return;
+      }
+
+      for (Resources::iterator it = resources_.begin();
+           it != resources_.end(); ++it)
+      {
+        if (it->second == NULL)
+        {
+          // This is resource is marked for expansion
+          std::list<std::string> children;
+          index.GetChildren(children, it->first);
+
+          std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
+
+          for (std::list<std::string>::const_iterator 
+                 it2 = children.begin(); it2 != children.end(); ++it2)
+          {
+            child->AddResourceToExpand(index, *it2);
+          }
+
+          it->second = child.release();
+        }
+
+        assert(it->second != NULL);
+        it->second->Expand(index);
+      }        
+    }
+
+
+    void Apply(IArchiveVisitor& visitor) const
+    {
+      if (level_ == ResourceType_Instance)
+      {
+        for (std::list<Instance>::const_iterator 
+               it = instances_.begin(); it != instances_.end(); ++it)
+        {
+          visitor.AddInstance(it->id_, it->dicom_);
+        }          
+      }
+      else
+      {
+        for (Resources::const_iterator it = resources_.begin();
+             it != resources_.end(); ++it)
+        {
+          assert(it->second != NULL);  // There must have been a call to "Expand()"
+          visitor.Open(level_, it->first);
+          it->second->Apply(visitor);
+          visitor.Close();
+        }
+      }
+    }
+  };
+
+
+
+  class ArchiveJob::ZipCommands : public boost::noncopyable
+  {
+  private:
+    enum Type
+    {
+      Type_OpenDirectory,
+      Type_CloseDirectory,
+      Type_WriteInstance
+    };
+
+    class Command : public boost::noncopyable
+    {
+    private:
+      Type          type_;
+      std::string   filename_;
+      std::string   instanceId_;
+      FileInfo      info_;
+
+    public:
+      explicit Command(Type type) :
+        type_(type)
+      {
+        assert(type_ == Type_CloseDirectory);
+      }
+        
+      Command(Type type,
+              const std::string& filename) :
+        type_(type),
+        filename_(filename)
+      {
+        assert(type_ == Type_OpenDirectory);
+      }
+        
+      Command(Type type,
+              const std::string& filename,
+              const std::string& instanceId,
+              const FileInfo& info) :
+        type_(type),
+        filename_(filename),
+        instanceId_(instanceId),
+        info_(info)
+      {
+        assert(type_ == Type_WriteInstance);
+      }
+        
+      void Apply(HierarchicalZipWriter& writer,
+                 ServerContext& context,
+                 DicomDirWriter* dicomDir,
+                 const std::string& dicomDirFolder) const
+      {
+        switch (type_)
+        {
+          case Type_OpenDirectory:
+            writer.OpenDirectory(filename_.c_str());
+            break;
+
+          case Type_CloseDirectory:
+            writer.CloseDirectory();
+            break;
+
+          case Type_WriteInstance:
+          {
+            std::string content;
+
+            try
+            {
+              context.ReadAttachment(content, info_);
+            }
+            catch (OrthancException& e)
+            {
+              LOG(WARNING) << "An instance was removed after the job was issued: " << instanceId_;
+              return;
+            }
+
+            //boost::this_thread::sleep(boost::posix_time::milliseconds(300));
+            
+            writer.OpenFile(filename_.c_str());
+            writer.Write(content);
+
+            if (dicomDir != NULL)
+            {
+              ParsedDicomFile parsed(content);
+              dicomDir->Add(dicomDirFolder, filename_, parsed);
+            }
+              
+            break;
+          }
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    };
+      
+    std::deque<Command*>  commands_;
+    uint64_t              uncompressedSize_;
+    unsigned int          instancesCount_;
+
+      
+    void ApplyInternal(HierarchicalZipWriter& writer,
+                       ServerContext& context,
+                       size_t index,
+                       DicomDirWriter* dicomDir,
+                       const std::string& dicomDirFolder) const
+    {
+      if (index >= commands_.size())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      commands_[index]->Apply(writer, context, dicomDir, dicomDirFolder);
+    }
+      
+  public:
+    ZipCommands() :
+      uncompressedSize_(0),
+      instancesCount_(0)
+    {
+    }
+      
+    ~ZipCommands()
+    {
+      for (std::deque<Command*>::iterator it = commands_.begin();
+           it != commands_.end(); ++it)
+      {
+        assert(*it != NULL);
+        delete *it;
+      }
+    }
+
+    size_t GetSize() const
+    {
+      return commands_.size();
+    }
+
+    unsigned int GetInstancesCount() const
+    {
+      return instancesCount_;
+    }
+
+    uint64_t GetUncompressedSize() const
+    {
+      return uncompressedSize_;
+    }
+
+    void Apply(HierarchicalZipWriter& writer,
+               ServerContext& context,
+               size_t index,
+               DicomDirWriter& dicomDir,
+               const std::string& dicomDirFolder) const
+    {
+      ApplyInternal(writer, context, index, &dicomDir, dicomDirFolder);
+    }
+
+    void Apply(HierarchicalZipWriter& writer,
+               ServerContext& context,
+               size_t index) const
+    {
+      ApplyInternal(writer, context, index, NULL, "");
+    }
+      
+    void AddOpenDirectory(const std::string& filename)
+    {
+      commands_.push_back(new Command(Type_OpenDirectory, filename));
+    }
+
+    void AddCloseDirectory()
+    {
+      commands_.push_back(new Command(Type_CloseDirectory));
+    }
+
+    void AddWriteInstance(const std::string& filename,
+                          const std::string& instanceId,
+                          const FileInfo& info)
+    {
+      commands_.push_back(new Command(Type_WriteInstance, filename, instanceId, info));
+      instancesCount_ ++;
+      uncompressedSize_ += info.GetUncompressedSize();
+    }
+
+    bool IsZip64() const
+    {
+      return IsZip64Required(GetUncompressedSize(), GetInstancesCount());
+    }
+  };
+    
+    
+
+  class ArchiveJob::ArchiveIndexVisitor : public IArchiveVisitor
+  {
+  private:
+    ZipCommands&    commands_;
+    ServerContext&  context_;
+    char            instanceFormat_[24];
+    unsigned int    counter_;
+
+    static std::string GetTag(const DicomMap& tags,
+                              const DicomTag& tag)
+    {
+      const DicomValue* v = tags.TestAndGetValue(tag);
+      if (v != NULL &&
+          !v->IsBinary() &&
+          !v->IsNull())
+      {
+        return v->GetContent();
+      }
+      else
+      {
+        return "";
+      }
+    }
+
+  public:
+    ArchiveIndexVisitor(ZipCommands& commands,
+                        ServerContext& context) :
+      commands_(commands),
+      context_(context),
+      counter_(0)
+    {
+      if (commands.GetSize() != 0)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+        
+      snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
+    }
+
+    virtual void Open(ResourceType level,
+                      const std::string& publicId)
+    {
+      std::string path;
+
+      DicomMap tags;
+      if (context_.GetIndex().GetMainDicomTags(tags, publicId, level, level))
+      {
+        switch (level)
+        {
+          case ResourceType_Patient:
+            path = GetTag(tags, DICOM_TAG_PATIENT_ID) + " " + GetTag(tags, DICOM_TAG_PATIENT_NAME);
+            break;
+
+          case ResourceType_Study:
+            path = GetTag(tags, DICOM_TAG_ACCESSION_NUMBER) + " " + GetTag(tags, DICOM_TAG_STUDY_DESCRIPTION);
+            break;
+
+          case ResourceType_Series:
+          {
+            std::string modality = GetTag(tags, DICOM_TAG_MODALITY);
+            path = modality + " " + GetTag(tags, DICOM_TAG_SERIES_DESCRIPTION);
+
+            if (modality.size() == 0)
+            {
+              snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
+            }
+            else if (modality.size() == 1)
+            {
+              snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%%07d.dcm", 
+                       toupper(modality[0]));
+            }
+            else if (modality.size() >= 2)
+            {
+              snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%c%%06d.dcm", 
+                       toupper(modality[0]), toupper(modality[1]));
+            }
+
+            counter_ = 0;
+
+            break;
+          }
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+
+      path = Toolbox::StripSpaces(Toolbox::ConvertToAscii(path));
+
+      if (path.empty())
+      {
+        path = std::string("Unknown ") + EnumerationToString(level);
+      }
+
+      commands_.AddOpenDirectory(path.c_str());
+    }
+
+    virtual void Close()
+    {
+      commands_.AddCloseDirectory();
+    }
+
+    virtual void AddInstance(const std::string& instanceId,
+                             const FileInfo& dicom)
+    {
+      char filename[24];
+      snprintf(filename, sizeof(filename) - 1, instanceFormat_, counter_);
+      counter_ ++;
+
+      commands_.AddWriteInstance(filename, instanceId, dicom);
+    }
+  };
+
+    
+  class ArchiveJob::MediaIndexVisitor : public IArchiveVisitor
+  {
+  private:
+    ZipCommands&    commands_;
+    ServerContext&  context_;
+    unsigned int    counter_;
+
+  public:
+    MediaIndexVisitor(ZipCommands& commands,
+                      ServerContext& context) :
+      commands_(commands),
+      context_(context),
+      counter_(0)
+    {
+    }
+
+    virtual void Open(ResourceType level,
+                      const std::string& publicId)
+    {
+    }
+
+    virtual void Close()
+    {
+    }
+
+    virtual void AddInstance(const std::string& instanceId,
+                             const FileInfo& dicom)
+    {
+      // "DICOM restricts the filenames on DICOM media to 8
+      // characters (some systems wrongly use 8.3, but this does not
+      // conform to the standard)."
+      std::string filename = "IM" + boost::lexical_cast<std::string>(counter_);
+      commands_.AddWriteInstance(filename, instanceId, dicom);
+
+      counter_ ++;
+    }
+  };
+
+
+  class ArchiveJob::ZipWriterIterator : public boost::noncopyable
+  {
+  private:
+    TemporaryFile&                        target_;
+    ServerContext&                        context_;
+    ZipCommands                           commands_;
+    std::auto_ptr<HierarchicalZipWriter>  zip_;
+    std::auto_ptr<DicomDirWriter>         dicomDir_;
+    bool                                  isMedia_;
+
+  public:
+    ZipWriterIterator(TemporaryFile& target,
+                      ServerContext& context,
+                      ArchiveIndex& archive,
+                      bool isMedia,
+                      bool enableExtendedSopClass) :
+      target_(target),
+      context_(context),
+      isMedia_(isMedia)
+    {
+      if (isMedia)
+      {
+        MediaIndexVisitor visitor(commands_, context);
+        archive.Expand(context.GetIndex());
+
+        commands_.AddOpenDirectory(MEDIA_IMAGES_FOLDER);        
+        archive.Apply(visitor);
+        commands_.AddCloseDirectory();
+
+        dicomDir_.reset(new DicomDirWriter);
+        dicomDir_->EnableExtendedSopClass(enableExtendedSopClass);
+      }
+      else
+      {
+        ArchiveIndexVisitor visitor(commands_, context);
+        archive.Expand(context.GetIndex());
+        archive.Apply(visitor);
+      }
+
+      zip_.reset(new HierarchicalZipWriter(target.GetPath().c_str()));
+      zip_->SetZip64(commands_.IsZip64());
+    }
+      
+    size_t GetStepsCount() const
+    {
+      return commands_.GetSize() + 1;
+    }
+
+    void RunStep(size_t index)
+    {
+      if (index > commands_.GetSize())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else if (index == commands_.GetSize())
+      {
+        // Last step: Add the DICOMDIR
+        if (isMedia_)
+        {
+          assert(dicomDir_.get() != NULL);
+          std::string s;
+          dicomDir_->Encode(s);
+
+          zip_->OpenFile("DICOMDIR");
+          zip_->Write(s);
+        }
+      }
+      else
+      {
+        if (isMedia_)
+        {
+          assert(dicomDir_.get() != NULL);
+          commands_.Apply(*zip_, context_, index, *dicomDir_, MEDIA_IMAGES_FOLDER);
+        }
+        else
+        {
+          assert(dicomDir_.get() == NULL);
+          commands_.Apply(*zip_, context_, index);
+        }
+      }
+    }
+
+    unsigned int GetInstancesCount() const
+    {
+      return commands_.GetInstancesCount();
+    }
+
+    uint64_t GetUncompressedSize() const
+    {
+      return commands_.GetUncompressedSize();
+    }
+  };
+
+
+  ArchiveJob::ArchiveJob(boost::shared_ptr<TemporaryFile>& target,
+                         ServerContext& context,
+                         bool isMedia,
+                         bool enableExtendedSopClass) :
+    target_(target),
+    context_(context),
+    archive_(new ArchiveIndex(ResourceType_Patient)),  // root
+    isMedia_(isMedia),
+    enableExtendedSopClass_(enableExtendedSopClass),
+    currentStep_(0),
+    instancesCount_(0),
+    uncompressedSize_(0)
+  {
+    if (target.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+  }
+
+
+  void ArchiveJob::AddResource(const std::string& publicId)
+  {
+    if (writer_.get() != NULL)   // Already started
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+        
+    ResourceIdentifiers resource(context_.GetIndex(), publicId);
+    archive_->Add(context_.GetIndex(), resource);
+  }
+
+  
+  void ArchiveJob::Reset()
+  {
+    LOG(ERROR) << "Cannot resubmit the creation of an archive";
+    throw OrthancException(ErrorCode_BadSequenceOfCalls);
+  }
+
+  
+  void ArchiveJob::Start()
+  {
+    if (writer_.get() != NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    writer_.reset(new ZipWriterIterator(*target_, context_, *archive_,
+                                        isMedia_, enableExtendedSopClass_));
+
+    instancesCount_ = writer_->GetInstancesCount();
+    uncompressedSize_ = writer_->GetUncompressedSize();
+  }
+
+  
+  JobStepResult ArchiveJob::Step()
+  {
+    assert(writer_.get() != NULL);
+
+    if (target_.unique())
+    {
+      LOG(WARNING) << "A client has disconnected while creating an archive";
+      return JobStepResult::Failure(ErrorCode_NetworkProtocol);          
+    }
+        
+    if (writer_->GetStepsCount() == 0)
+    {
+      writer_.reset();  // Flush all the results
+      return JobStepResult::Success();
+    }
+    else
+    {
+      writer_->RunStep(currentStep_);
+
+      currentStep_ ++;
+
+      if (currentStep_ == writer_->GetStepsCount())
+      {
+        writer_.reset();  // Flush all the results
+        return JobStepResult::Success();
+      }
+      else
+      {
+        return JobStepResult::Continue();
+      }
+    }
+  }
+
+
+  float ArchiveJob::GetProgress()
+  {
+    if (writer_.get() == NULL ||
+        writer_->GetStepsCount() == 0)
+    {
+      return 1;
+    }
+    else
+    {
+      return (static_cast<float>(currentStep_) /
+              static_cast<float>(writer_->GetStepsCount() - 1));
+    }
+  }
+
+    
+  void ArchiveJob::GetJobType(std::string& target)
+  {
+    if (isMedia_)
+    {
+      target = "Media";
+    }
+    else
+    {
+      target = "Archive";
+    }
+  }
+
+    
+  void ArchiveJob::GetPublicContent(Json::Value& value)
+  {
+    value["Description"] = description_;
+    value["InstancesCount"] = instancesCount_;
+    value["UncompressedSizeMB"] =
+      static_cast<unsigned int>(uncompressedSize_ / MEGA_BYTES);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/ArchiveJob.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,104 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/JobsEngine/IJob.h"
+#include "../../Core/TemporaryFile.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class ArchiveJob : public IJob
+  {
+  private:
+    class ArchiveIndex;
+    class ArchiveIndexVisitor;
+    class IArchiveVisitor;
+    class MediaIndexVisitor;
+    class ResourceIdentifiers;
+    class ZipCommands;
+    class ZipWriterIterator;
+    
+    boost::shared_ptr<TemporaryFile>      target_;
+    ServerContext&                        context_;
+    boost::shared_ptr<ArchiveIndex>       archive_;
+    bool                                  isMedia_;
+    bool                                  enableExtendedSopClass_;
+    std::string                           description_;
+
+    boost::shared_ptr<ZipWriterIterator>  writer_;
+    size_t                                currentStep_;
+    unsigned int                          instancesCount_;
+    uint64_t                              uncompressedSize_;
+
+  public:
+    ArchiveJob(boost::shared_ptr<TemporaryFile>& target,
+               ServerContext& context,
+               bool isMedia,
+               bool enableExtendedSopClass);
+
+    void SetDescription(const std::string& description)
+    {
+      description_ = description;
+    }
+
+    const std::string& GetDescription() const
+    {
+      return description_;
+    }
+
+    void AddResource(const std::string& publicId);
+
+    virtual void Reset();
+
+    virtual void Start();
+
+    virtual JobStepResult Step();
+
+    virtual void Stop(JobStopReason reason)
+    {
+    }
+
+    virtual float GetProgress();
+
+    virtual void GetJobType(std::string& target);
+    
+    virtual void GetPublicContent(Json::Value& value);
+
+    virtual bool Serialize(Json::Value& value)
+    {
+      return false;  // Cannot serialize this kind of job
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,228 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "DicomModalityStoreJob.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/SerializationToolbox.h"
+
+namespace Orthanc
+{
+  void DicomModalityStoreJob::OpenConnection()
+  {
+    if (connection_.get() == NULL)
+    {
+      connection_.reset(new DicomUserConnection);
+      connection_->SetLocalApplicationEntityTitle(localAet_);
+      connection_->SetRemoteModality(remote_);
+    }
+  }
+
+
+  bool DicomModalityStoreJob::HandleInstance(const std::string& instance)
+  {
+    assert(IsStarted());
+    OpenConnection();
+
+    LOG(INFO) << "Sending instance " << instance << " to modality \"" 
+              << remote_.GetApplicationEntityTitle() << "\"";
+
+    std::string dicom;
+
+    try
+    {
+      context_.ReadDicom(dicom, instance);
+    }
+    catch (OrthancException& e)
+    {
+      LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
+      return false;
+    }
+
+    if (HasMoveOriginator())
+    {
+      connection_->Store(dicom, moveOriginatorAet_, moveOriginatorId_);
+    }
+    else
+    {
+      connection_->Store(dicom);
+    }
+
+    //boost::this_thread::sleep(boost::posix_time::milliseconds(500));
+
+    return true;
+  }
+    
+
+  bool DicomModalityStoreJob::HandleTrailingStep()
+  {
+    throw OrthancException(ErrorCode_InternalError);
+  }
+
+
+  DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) :
+    context_(context),
+    localAet_("ORTHANC"),
+    moveOriginatorId_(0)  // By default, not a C-MOVE
+  {
+  }
+
+
+  void DicomModalityStoreJob::SetLocalAet(const std::string& aet)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      localAet_ = aet;
+    }
+  }
+
+
+  void DicomModalityStoreJob::SetRemoteModality(const RemoteModalityParameters& remote)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      remote_ = remote;
+    }
+  }
+
+    
+  const std::string& DicomModalityStoreJob::GetMoveOriginatorAet() const
+  {
+    if (HasMoveOriginator())
+    {
+      return moveOriginatorAet_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+    
+  uint16_t DicomModalityStoreJob::GetMoveOriginatorId() const
+  {
+    if (HasMoveOriginator())
+    {
+      return moveOriginatorId_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void DicomModalityStoreJob::SetMoveOriginator(const std::string& aet,
+                                                int id)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (id < 0 || 
+             id >= 65536)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      moveOriginatorId_ = static_cast<uint16_t>(id);
+      moveOriginatorAet_ = aet;
+    }
+  }
+
+  void DicomModalityStoreJob::Stop(JobStopReason reason)   // For pausing jobs
+  {
+    connection_.reset(NULL);
+  }
+
+
+  void DicomModalityStoreJob::GetPublicContent(Json::Value& value)
+  {
+    SetOfInstancesJob::GetPublicContent(value);
+    
+    value["LocalAet"] = localAet_;
+    value["RemoteAet"] = remote_.GetApplicationEntityTitle();
+
+    if (HasMoveOriginator())
+    {
+      value["MoveOriginatorAET"] = GetMoveOriginatorAet();
+      value["MoveOriginatorID"] = GetMoveOriginatorId();
+    }
+  }
+
+
+  static const char* LOCAL_AET = "LocalAet";
+  static const char* REMOTE = "Remote";
+  static const char* MOVE_ORIGINATOR_AET = "MoveOriginatorAet";
+  static const char* MOVE_ORIGINATOR_ID = "MoveOriginatorId";
+  
+
+  DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context,
+                                               const Json::Value& serialized) :
+    SetOfInstancesJob(serialized),
+    context_(context)
+  {
+    localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET);
+    remote_ = RemoteModalityParameters(serialized[REMOTE]);
+    moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET);
+    moveOriginatorId_ = static_cast<uint16_t>
+      (SerializationToolbox::ReadUnsignedInteger(serialized, MOVE_ORIGINATOR_ID));
+  }
+
+
+  bool DicomModalityStoreJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfInstancesJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      target[LOCAL_AET] = localAet_;
+      remote_.Serialize(target[REMOTE], true /* force advanced format */);
+      target[MOVE_ORIGINATOR_AET] = moveOriginatorAet_;
+      target[MOVE_ORIGINATOR_ID] = moveOriginatorId_;
+      return true;
+    }
+  }  
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,103 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/JobsEngine/SetOfInstancesJob.h"
+#include "../../Core/DicomNetworking/DicomUserConnection.h"
+
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class DicomModalityStoreJob : public SetOfInstancesJob
+  {
+  private:
+    ServerContext&                      context_;
+    std::string                         localAet_;
+    RemoteModalityParameters            remote_;
+    std::string                         moveOriginatorAet_;
+    uint16_t                            moveOriginatorId_;
+    std::auto_ptr<DicomUserConnection>  connection_;
+
+    void OpenConnection();
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance);
+    
+    virtual bool HandleTrailingStep();
+
+  public:
+    DicomModalityStoreJob(ServerContext& context);
+
+    DicomModalityStoreJob(ServerContext& context,
+                          const Json::Value& serialized);
+
+    const std::string& GetLocalAet() const
+    {
+      return localAet_;
+    }
+
+    void SetLocalAet(const std::string& aet);
+
+    const RemoteModalityParameters& GetRemoteModality() const
+    {
+      return remote_;
+    }
+
+    void SetRemoteModality(const RemoteModalityParameters& remote);
+
+    bool HasMoveOriginator() const
+    {
+      return moveOriginatorId_ != 0;
+    }
+    
+    const std::string& GetMoveOriginatorAet() const;
+    
+    uint16_t GetMoveOriginatorId() const;
+
+    void SetMoveOriginator(const std::string& aet,
+                           int id);
+
+    virtual void Stop(JobStopReason reason);
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = "DicomModalityStore";
+    }
+
+    virtual void GetPublicContent(Json::Value& value);
+
+    virtual bool Serialize(Json::Value& target);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/DicomMoveScuJob.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,199 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomMoveScuJob.h"
+
+#include "../../Core/SerializationToolbox.h"
+
+namespace Orthanc
+{
+  class DicomMoveScuJob::Command : public SetOfCommandsJob::ICommand
+  {
+  private:
+    DicomMoveScuJob&         that_;
+    std::auto_ptr<DicomMap>  findAnswer_;
+
+  public:
+    Command(DicomMoveScuJob& that,
+            const DicomMap&  findAnswer) :
+      that_(that),
+      findAnswer_(findAnswer.Clone())
+    {
+    }
+
+    virtual bool Execute()
+    {
+      that_.Retrieve(*findAnswer_);
+      return true;
+    }
+
+    virtual void Serialize(Json::Value& target) const
+    {
+      findAnswer_->Serialize(target);
+    }
+  };
+
+
+  class DicomMoveScuJob::Unserializer :
+    public SetOfCommandsJob::ICommandUnserializer
+  {
+  private:
+    DicomMoveScuJob&   that_;
+
+  public:
+    Unserializer(DicomMoveScuJob&  that) :
+      that_(that)
+    {
+    }
+
+    virtual ICommand* Unserialize(const Json::Value& source) const
+    {
+      DicomMap findAnswer;
+      findAnswer.Unserialize(source);
+      return new Command(that_, findAnswer);
+    }
+  };
+
+
+
+  void DicomMoveScuJob::Retrieve(const DicomMap& findAnswer)
+  {
+    if (connection_.get() == NULL)
+    {
+      connection_.reset(new DicomUserConnection(localAet_, remote_));
+      connection_->Open();
+    }
+    
+    connection_->Move(targetAet_, findAnswer);
+  }
+    
+
+  void DicomMoveScuJob::AddFindAnswer(const DicomMap& answer)
+  {
+    AddCommand(new Command(*this, answer));
+  }
+
+  
+  void DicomMoveScuJob::AddFindAnswer(QueryRetrieveHandler& query,
+                                      size_t i)
+  {
+    DicomMap answer;
+    query.GetAnswer(answer, i);
+    AddFindAnswer(answer);
+  }    
+
+
+  void DicomMoveScuJob::SetLocalAet(const std::string& aet)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      localAet_ = aet;
+    }
+  }
+
+  
+  void DicomMoveScuJob::SetTargetAet(const std::string& aet)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      targetAet_ = aet;
+    }
+  }
+
+  
+  void DicomMoveScuJob::SetRemoteModality(const RemoteModalityParameters& remote)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      remote_ = remote;
+    }
+  }
+
+  
+  void DicomMoveScuJob::Stop(JobStopReason reason)
+  {
+    connection_.reset();
+  }
+  
+
+  void DicomMoveScuJob::GetPublicContent(Json::Value& value)
+  {
+    SetOfCommandsJob::GetPublicContent(value);
+    
+    value["LocalAet"] = localAet_;
+    value["RemoteAet"] = remote_.GetApplicationEntityTitle();
+  }
+
+
+  static const char* LOCAL_AET = "LocalAet";
+  static const char* TARGET_AET = "TargetAet";
+  static const char* REMOTE = "Remote";
+
+  DicomMoveScuJob::DicomMoveScuJob(ServerContext& context,
+                                   const Json::Value& serialized) :
+    SetOfCommandsJob(new Unserializer(*this), serialized),
+    context_(context)
+  {
+    localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET);
+    targetAet_ = SerializationToolbox::ReadString(serialized, TARGET_AET);
+    remote_ = RemoteModalityParameters(serialized[REMOTE]);
+  }
+
+  
+  bool DicomMoveScuJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfCommandsJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      target[LOCAL_AET] = localAet_;
+      target[TARGET_AET] = targetAet_;
+      remote_.Serialize(target[REMOTE], true /* force advanced format */);
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/DicomMoveScuJob.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,104 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/JobsEngine/SetOfCommandsJob.h"
+#include "../../Core/DicomNetworking/DicomUserConnection.h"
+
+#include "../QueryRetrieveHandler.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class DicomMoveScuJob : public SetOfCommandsJob
+  {
+  private:
+    class Command;
+    class Unserializer;
+    
+    ServerContext&                      context_;
+    std::string                         localAet_;
+    std::string                         targetAet_;
+    RemoteModalityParameters            remote_;
+    std::auto_ptr<DicomUserConnection>  connection_;
+
+    void Retrieve(const DicomMap& findAnswer);
+    
+  public:
+    DicomMoveScuJob(ServerContext& context) :
+      context_(context)
+    {
+    }
+
+    DicomMoveScuJob(ServerContext& context,
+                    const Json::Value& serialized);
+
+    void AddFindAnswer(const DicomMap& answer);
+    
+    void AddFindAnswer(QueryRetrieveHandler& query,
+                       size_t i);
+    
+    const std::string& GetLocalAet() const
+    {
+      return localAet_;
+    }
+
+    void SetLocalAet(const std::string& aet);
+
+    const std::string& GetTargetAet() const
+    {
+      return targetAet_;
+    }
+
+    void SetTargetAet(const std::string& aet);
+
+    const RemoteModalityParameters& GetRemoteModality() const
+    {
+      return remote_;
+    }
+
+    void SetRemoteModality(const RemoteModalityParameters& remote);
+
+    virtual void Stop(JobStopReason reason);
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = "DicomMoveScu";
+    }
+
+    virtual void GetPublicContent(Json::Value& value);
+
+    virtual bool Serialize(Json::Value& target);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/LuaJobManager.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,271 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "LuaJobManager.h"
+
+#include "../OrthancInitialization.h"
+#include "../../Core/Logging.h"
+
+#include "../../Core/JobsEngine/Operations/LogJobOperation.h"
+#include "Operations/DeleteResourceOperation.h"
+#include "Operations/ModifyInstanceOperation.h"
+#include "Operations/StorePeerOperation.h"
+#include "Operations/StoreScuOperation.h"
+#include "Operations/SystemCallOperation.h"
+
+#include "../../Core/JobsEngine/Operations/NullOperationValue.h"
+#include "../../Core/JobsEngine/Operations/StringOperationValue.h"
+#include "Operations/DicomInstanceOperationValue.h"
+
+namespace Orthanc
+{
+  void LuaJobManager::SignalDone(const SequenceOfOperationsJob& job)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (&job == currentJob_)
+    {
+      currentId_.clear();
+      currentJob_ = NULL;
+    }
+  }
+
+
+  LuaJobManager::LuaJobManager() :
+    currentJob_(NULL),
+    maxOperations_(1000),
+    priority_(0),
+    trailingTimeout_(5000)
+  {
+    dicomTimeout_ = Configuration::GetGlobalUnsignedIntegerParameter("DicomAssociationCloseDelay", 5);
+    LOG(INFO) << "Lua: DICOM associations will be closed after "
+              << dicomTimeout_ << " seconds of inactivity";
+  }
+
+
+  void LuaJobManager::SetMaxOperationsPerJob(size_t count)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    maxOperations_ = count;
+  }
+
+
+  void LuaJobManager::SetPriority(int priority)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    priority_ = priority;
+  }
+
+
+  void LuaJobManager::SetTrailingOperationTimeout(unsigned int timeout)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    trailingTimeout_ = timeout;
+  }
+
+
+  void LuaJobManager::AwakeTrailingSleep()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    LOG(INFO) << "Awaking trailing sleep";
+
+    if (currentJob_ != NULL)
+    {
+      currentJob_->AwakeTrailingSleep();
+    }
+  }
+
+
+  LuaJobManager::Lock::Lock(LuaJobManager& that,
+                            JobsEngine& engine) :
+    that_(that),
+    lock_(that.mutex_),
+    engine_(engine)
+  {
+    if (that_.currentJob_ == NULL)
+    {
+      isNewJob_ = true;
+    }
+    else
+    {
+      jobLock_.reset(new SequenceOfOperationsJob::Lock(*that_.currentJob_));
+
+      if (jobLock_->IsDone() ||
+          jobLock_->GetOperationsCount() >= that_.maxOperations_)
+      {
+        jobLock_.reset(NULL);
+        isNewJob_ = true;
+      }
+      else
+      {
+        isNewJob_ = false;
+      }
+    }
+
+    if (isNewJob_)
+    {
+      // Need to create a new job, as the previous one is either
+      // finished, or is getting too long
+      that_.currentJob_ = new SequenceOfOperationsJob;
+      that_.currentJob_->SetDescription("Lua");
+
+      {
+        jobLock_.reset(new SequenceOfOperationsJob::Lock(*that_.currentJob_));
+        jobLock_->SetTrailingOperationTimeout(that_.trailingTimeout_);
+        jobLock_->SetDicomAssociationTimeout(that_.dicomTimeout_ * 1000);  // Milliseconds expected
+      }
+    }
+
+    assert(jobLock_.get() != NULL);
+  }
+
+
+  LuaJobManager::Lock::~Lock()
+  {
+    bool isEmpty;
+    
+    assert(jobLock_.get() != NULL);
+    isEmpty = (isNewJob_ &&
+               jobLock_->GetOperationsCount() == 0);
+    
+    jobLock_.reset(NULL);
+
+    if (isNewJob_)
+    {
+      if (isEmpty)
+      {
+        // No operation was added, discard the newly created job
+        isNewJob_ = false;
+        delete that_.currentJob_;
+        that_.currentJob_ = NULL;
+      }
+      else
+      {
+        engine_.GetRegistry().Submit(that_.currentId_, that_.currentJob_, that_.priority_);
+      }
+    }
+  }
+
+
+  size_t LuaJobManager::Lock::AddDeleteResourceOperation(ServerContext& context)
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation(new DeleteResourceOperation(context));
+  }
+
+
+  size_t LuaJobManager::Lock::AddLogOperation()
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation(new LogJobOperation);
+  }
+
+
+  size_t LuaJobManager::Lock::AddStoreScuOperation(const std::string& localAet,
+                                                   const RemoteModalityParameters& modality)
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation(new StoreScuOperation(localAet, modality));    
+  }
+
+
+  size_t LuaJobManager::Lock::AddStorePeerOperation(const WebServiceParameters& peer)
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation(new StorePeerOperation(peer));    
+  }
+
+
+  size_t LuaJobManager::Lock::AddSystemCallOperation(const std::string& command)
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation(new SystemCallOperation(command));    
+  }
+ 
+
+  size_t LuaJobManager::Lock::AddSystemCallOperation
+  (const std::string& command,
+   const std::vector<std::string>& preArguments,
+   const std::vector<std::string>& postArguments)
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation
+      (new SystemCallOperation(command, preArguments, postArguments));
+  }
+
+
+  size_t LuaJobManager::Lock::AddModifyInstanceOperation(ServerContext& context,
+                                                         DicomModification* modification)
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation
+      (new ModifyInstanceOperation(context, RequestOrigin_Lua, modification));
+  }
+
+
+  void LuaJobManager::Lock::AddNullInput(size_t operation)
+  {
+    assert(jobLock_.get() != NULL);
+    NullOperationValue null;
+    jobLock_->AddInput(operation, null);
+  }
+
+
+  void LuaJobManager::Lock::AddStringInput(size_t operation,
+                                           const std::string& content)
+  {
+    assert(jobLock_.get() != NULL);
+    StringOperationValue value(content);
+    jobLock_->AddInput(operation, value);
+  }
+
+
+  void LuaJobManager::Lock::AddDicomInstanceInput(size_t operation,
+                                                  ServerContext& context,
+                                                  const std::string& instanceId)
+  {
+    assert(jobLock_.get() != NULL);
+    DicomInstanceOperationValue value(context, instanceId);
+    jobLock_->AddInput(operation, value);
+  }
+
+
+  void LuaJobManager::Lock::Connect(size_t operation1,
+                                    size_t operation2)
+  {
+    assert(jobLock_.get() != NULL);
+    jobLock_->Connect(operation1, operation2);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/LuaJobManager.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,115 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/DicomParsing/DicomModification.h"
+#include "../../Core/JobsEngine/JobsEngine.h"
+#include "../../Core/JobsEngine/Operations/SequenceOfOperationsJob.h"
+#include "../../Core/WebServiceParameters.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class LuaJobManager : private SequenceOfOperationsJob::IObserver
+  {
+  private:
+    boost::mutex              mutex_;
+    std::string               currentId_;
+    SequenceOfOperationsJob*  currentJob_;
+    size_t                    maxOperations_;
+    int                       priority_;
+    unsigned int              trailingTimeout_;
+    unsigned int              dicomTimeout_;
+
+    virtual void SignalDone(const SequenceOfOperationsJob& job);
+
+  public:
+    LuaJobManager();
+
+    void SetMaxOperationsPerJob(size_t count);
+
+    void SetPriority(int priority);
+
+    void SetTrailingOperationTimeout(unsigned int timeout);
+
+    void AwakeTrailingSleep();
+
+    class Lock : public boost::noncopyable
+    {
+    private:
+      LuaJobManager&                                that_;
+      boost::mutex::scoped_lock                     lock_;
+      JobsEngine&                                   engine_;
+      std::auto_ptr<SequenceOfOperationsJob::Lock>  jobLock_;
+      bool                                          isNewJob_;
+
+    public:
+      Lock(LuaJobManager& that,
+           JobsEngine& engine);
+
+      ~Lock();
+
+      size_t AddLogOperation();
+
+      size_t AddDeleteResourceOperation(ServerContext& context);
+
+      size_t AddStoreScuOperation(const std::string& localAet,
+                                  const RemoteModalityParameters& modality);
+
+      size_t AddStorePeerOperation(const WebServiceParameters& peer);
+
+      size_t AddSystemCallOperation(const std::string& command);
+
+      size_t AddSystemCallOperation(const std::string& command,
+                                    const std::vector<std::string>& preArguments,
+                                    const std::vector<std::string>& postArguments);
+
+      size_t AddModifyInstanceOperation(ServerContext& context,
+                                        DicomModification* modification);
+
+      void AddNullInput(size_t operation); 
+
+      void AddStringInput(size_t operation,
+                          const std::string& content);
+
+      void AddDicomInstanceInput(size_t operation,
+                                 ServerContext& context,
+                                 const std::string& instanceId);
+
+      void Connect(size_t operation1,
+                   size_t operation2);
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/MergeStudyJob.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,395 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "MergeStudyJob.h"
+
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/Logging.h"
+#include "../../Core/SerializationToolbox.h"
+
+
+namespace Orthanc
+{
+  void MergeStudyJob::AddSourceSeriesInternal(const std::string& series)
+  {
+    // Generate a target SeriesInstanceUID for this series
+    seriesUidMap_[series] = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series);
+
+    // Add all the instances of the series as to be processed
+    std::list<std::string> instances;
+    context_.GetIndex().GetChildren(instances, series);
+
+    for (std::list<std::string>::const_iterator
+           it = instances.begin(); it != instances.end(); ++it)
+    {
+      AddInstance(*it);
+    }
+  }
+
+
+  void MergeStudyJob::AddSourceStudyInternal(const std::string& study)
+  {
+    if (study == targetStudy_)
+    {
+      LOG(ERROR) << "Cannot merge a study into the same study: " << study;
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+    else
+    {
+      std::list<std::string> series;
+      context_.GetIndex().GetChildren(series, study);
+
+      for (std::list<std::string>::const_iterator
+             it = series.begin(); it != series.end(); ++it)
+      {
+        AddSourceSeriesInternal(*it);
+      }
+    }
+  }
+
+
+  bool MergeStudyJob::HandleInstance(const std::string& instance)
+  {
+    /**
+     * Retrieve the DICOM instance to be modified
+     **/
+    
+    std::auto_ptr<ParsedDicomFile> modified;
+
+    try
+    {
+      ServerContext::DicomCacheLocker locker(context_, instance);
+      modified.reset(locker.GetDicom().Clone(true));
+    }
+    catch (OrthancException&)
+    {
+      LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
+      return false;
+    }
+
+
+    /**
+     * Chose the target UIDs
+     **/
+
+    std::string series = modified->GetHasher().HashSeries();
+
+    SeriesUidMap::const_iterator targetSeriesUid = seriesUidMap_.find(series);
+
+    if (targetSeriesUid == seriesUidMap_.end())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);  // Should never happen
+    }
+
+
+    /**
+     * Copy the tags from the "Patient Module Attributes" and "General
+     * Study Module Attributes" modules of the target study
+     **/
+
+    for (std::set<DicomTag>::const_iterator it = removals_.begin();
+         it != removals_.end(); ++it)
+    {
+      modified->Remove(*it);
+    }
+    
+    for (Replacements::const_iterator it = replacements_.begin();
+         it != replacements_.end(); ++it)
+    {
+      modified->ReplacePlainString(it->first, it->second);
+    }
+
+
+    /**
+     * Store the new instance into Orthanc
+     **/
+    
+    modified->ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, targetSeriesUid->second);
+
+    DicomInstanceToStore toStore;
+    toStore.SetOrigin(origin_);
+    toStore.SetParsedDicomFile(*modified);
+
+    std::string modifiedInstance;
+    if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success)
+    {
+      LOG(ERROR) << "Error while storing a modified instance " << instance;
+      return false;
+    }
+
+    return true;
+  }
+
+  
+  bool MergeStudyJob::HandleTrailingStep()
+  {
+    if (!keepSource_)
+    {
+      const size_t n = GetInstancesCount();
+
+      for (size_t i = 0; i < n; i++)
+      {
+        Json::Value tmp;
+        context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance);
+      }
+    }
+
+    return true;
+  }
+
+  
+  MergeStudyJob::MergeStudyJob(ServerContext& context,
+                               const std::string& targetStudy) :
+    context_(context),
+    keepSource_(false),
+    targetStudy_(targetStudy)
+  {
+    /**
+     * Check the validity of the input ID
+     **/
+    
+    ResourceType type;
+
+    if (!context_.GetIndex().LookupResourceType(type, targetStudy) ||
+        type != ResourceType_Study)
+    {
+      LOG(ERROR) << "Cannot merge into an unknown study: " << targetStudy;
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+
+    /**
+     * Detect the tags to be removed/replaced by parsing one child
+     * instance of the study
+     **/
+
+    DicomTag::AddTagsForModule(removals_, DicomModule_Patient);
+    DicomTag::AddTagsForModule(removals_, DicomModule_Study);
+    
+    std::list<std::string> instances;
+    context_.GetIndex().GetChildInstances(instances, targetStudy);
+    
+    if (instances.empty())
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    DicomMap dicom;
+
+    {
+      ServerContext::DicomCacheLocker locker(context_, instances.front());
+      locker.GetDicom().ExtractDicomSummary(dicom);
+    }
+
+    const std::set<DicomTag> moduleTags = removals_;
+    for (std::set<DicomTag>::const_iterator it = moduleTags.begin();
+         it != moduleTags.end(); ++it)
+    {
+      const DicomValue* value = dicom.TestAndGetValue(*it);
+      std::string str;
+      
+      if (value != NULL &&
+          value->CopyToString(str, false))
+      {
+        removals_.erase(*it);
+        replacements_.insert(std::make_pair(*it, str));
+      }
+    }
+  }
+  
+
+  void MergeStudyJob::SetOrigin(const DicomInstanceOrigin& origin)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      origin_ = origin;
+    }
+  }
+
+  
+  void MergeStudyJob::SetOrigin(const RestApiCall& call)
+  {
+    SetOrigin(DicomInstanceOrigin::FromRest(call));
+  }
+
+
+  void MergeStudyJob::AddSource(const std::string& studyOrSeries)
+  {
+    ResourceType level;
+    
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (!context_.GetIndex().LookupResourceType(level, studyOrSeries))
+    {
+      LOG(ERROR) << "Cannot find this resource: " << studyOrSeries;
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+    else
+    {
+      switch (level)
+      {
+        case ResourceType_Study:
+          AddSourceStudyInternal(studyOrSeries);
+          break;
+          
+        case ResourceType_Series:
+          AddSourceSeries(studyOrSeries);
+          break;
+          
+        default:
+          LOG(ERROR) << "This resource is neither a study, nor a series: "
+                     << studyOrSeries << " is a " << EnumerationToString(level);
+          throw OrthancException(ErrorCode_UnknownResource);
+      }
+    }    
+  }
+  
+
+  void MergeStudyJob::AddSourceSeries(const std::string& series)
+  {
+    std::string parent;
+
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study))
+    {
+      LOG(ERROR) << "This resource is not a series: " << series;
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+    else if (parent == targetStudy_)
+    {
+      LOG(ERROR) << "Cannot merge series " << series
+                 << " into its parent study " << targetStudy_;
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+    else
+    {
+      AddSourceSeriesInternal(series);
+    }    
+  }
+
+
+  void MergeStudyJob::AddSourceStudy(const std::string& study)
+  {
+    ResourceType actualLevel;
+
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (!context_.GetIndex().LookupResourceType(actualLevel, study) ||
+             actualLevel != ResourceType_Study)
+    {
+      LOG(ERROR) << "This resource is not a study: " << study;
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+    else
+    {
+      AddSourceStudyInternal(study);
+    }    
+  }
+
+
+  void MergeStudyJob::SetKeepSource(bool keep)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    keepSource_ = keep;
+  }
+
+
+  void MergeStudyJob::GetPublicContent(Json::Value& value)
+  {
+    SetOfInstancesJob::GetPublicContent(value);
+    value["TargetStudy"] = targetStudy_;
+  }
+
+
+  static const char* KEEP_SOURCE = "KeepSource";
+  static const char* TARGET_STUDY = "TargetStudy";
+  static const char* REPLACEMENTS = "Replacements";
+  static const char* REMOVALS = "Removals";
+  static const char* SERIES_UID_MAP = "SeriesUIDMap";
+  static const char* ORIGIN = "Origin";
+
+
+  MergeStudyJob::MergeStudyJob(ServerContext& context,
+                               const Json::Value& serialized) :
+    SetOfInstancesJob(serialized),  // (*)
+    context_(context)
+  {
+    if (!HasTrailingStep())
+    {
+      // Should have been set by (*)
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE);
+    targetStudy_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY);
+    SerializationToolbox::ReadMapOfTags(replacements_, serialized, REPLACEMENTS);
+    SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS);
+    SerializationToolbox::ReadMapOfStrings(seriesUidMap_, serialized, SERIES_UID_MAP);
+    origin_ = DicomInstanceOrigin(serialized[ORIGIN]);
+  }
+
+  
+  bool MergeStudyJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfInstancesJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      target[KEEP_SOURCE] = keepSource_;
+      target[TARGET_STUDY] = targetStudy_;
+      SerializationToolbox::WriteMapOfTags(target, replacements_, REPLACEMENTS);
+      SerializationToolbox::WriteSetOfTags(target, removals_, REMOVALS);
+      SerializationToolbox::WriteMapOfStrings(target, seriesUidMap_, SERIES_UID_MAP);
+      origin_.Serialize(target[ORIGIN]);
+
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/MergeStudyJob.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,115 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/JobsEngine/SetOfInstancesJob.h"
+
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class MergeStudyJob : public SetOfInstancesJob
+  {
+  private:
+    typedef std::map<std::string, std::string>  SeriesUidMap;
+    typedef std::map<DicomTag, std::string>     Replacements;
+    
+    
+    ServerContext&         context_;
+    bool                   keepSource_;
+    std::string            targetStudy_;
+    Replacements           replacements_;
+    std::set<DicomTag>     removals_;
+    SeriesUidMap           seriesUidMap_;
+    DicomInstanceOrigin    origin_;
+
+
+    void AddSourceSeriesInternal(const std::string& series);
+
+    void AddSourceStudyInternal(const std::string& study);
+
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance);
+
+    virtual bool HandleTrailingStep();
+    
+  public:
+    MergeStudyJob(ServerContext& context,
+                  const std::string& targetStudy);
+
+    MergeStudyJob(ServerContext& context,
+                  const Json::Value& serialized);
+
+    const std::string& GetTargetStudy() const
+    {
+      return targetStudy_;
+    }
+
+    void AddSource(const std::string& studyOrSeries);
+
+    void AddSourceStudy(const std::string& study);
+
+    void AddSourceSeries(const std::string& series);
+
+    bool IsKeepSource() const
+    {
+      return keepSource_;
+    }
+    
+    void SetKeepSource(bool keep);
+
+    void SetOrigin(const DicomInstanceOrigin& origin);
+
+    void SetOrigin(const RestApiCall& call);
+
+    const DicomInstanceOrigin& GetOrigin() const
+    {
+      return origin_;
+    }
+
+    virtual void Stop(JobStopReason reason)
+    {
+    }
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = "MergeStudy";
+    }
+
+    virtual void GetPublicContent(Json::Value& value);
+
+    virtual bool Serialize(Json::Value& target);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeadersServer.h"
+#include "DeleteResourceOperation.h"
+
+#include "DicomInstanceOperationValue.h"
+
+#include "../../../Core/Logging.h"
+#include "../../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  void DeleteResourceOperation::Apply(JobOperationValues& outputs,
+                                      const JobOperationValue& input,
+                                      IDicomConnectionManager& connectionManager)
+  {
+    switch (input.GetType())
+    {
+      case JobOperationValue::Type_DicomInstance:
+      {
+        const DicomInstanceOperationValue& instance = dynamic_cast<const DicomInstanceOperationValue&>(input);
+        LOG(INFO) << "Lua: Deleting instance: " << instance.GetId();
+
+        try
+        {
+          Json::Value tmp;
+          context_.DeleteResource(tmp, instance.GetId(), ResourceType_Instance);
+        }
+        catch (OrthancException& e)
+        {
+          LOG(ERROR) << "Lua: Unable to delete instance " << instance.GetId() << ": " << e.What();
+        }
+
+        break;
+      }
+
+      default:
+        break;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,64 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../Core/JobsEngine/Operations/IJobOperation.h"
+
+#include "../../ServerContext.h"
+
+namespace Orthanc
+{
+  class DeleteResourceOperation : public IJobOperation
+  {
+  private:
+    ServerContext&  context_;
+
+  public:
+    DeleteResourceOperation(ServerContext& context) :
+    context_(context)
+    {
+    }
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& connectionManager);
+
+    virtual void Serialize(Json::Value& result) const
+    {
+      result = Json::objectValue;
+      result["Type"] = "DeleteResource";
+    }
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,84 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../Core/JobsEngine/Operations/JobOperationValue.h"
+
+#include "../../ServerContext.h"
+
+namespace Orthanc
+{
+  class DicomInstanceOperationValue : public JobOperationValue
+  {
+  private:
+    ServerContext&   context_;
+    std::string      id_;
+
+  public:
+    DicomInstanceOperationValue(ServerContext& context,
+                                const std::string& id) :
+      JobOperationValue(Type_DicomInstance),
+      context_(context),
+      id_(id)
+    {
+    }
+
+    ServerContext& GetServerContext() const
+    {
+      return context_;
+    }
+
+    const std::string& GetId() const
+    {
+      return id_;
+    }
+
+    void ReadDicom(std::string& dicom) const
+    {
+      context_.ReadDicom(dicom, id_);
+    }
+
+    virtual JobOperationValue* Clone() const
+    {
+      return new DicomInstanceOperationValue(context_, id_);
+    }
+
+    virtual void Serialize(Json::Value& target) const
+    {
+      target = Json::objectValue;
+      target["Type"] = "DicomInstance";
+      target["ID"] = id_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,153 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeadersServer.h"
+#include "ModifyInstanceOperation.h"
+
+#include "DicomInstanceOperationValue.h"
+
+#include "../../../Core/Logging.h"
+#include "../../../Core/SerializationToolbox.h"
+
+namespace Orthanc
+{
+  ModifyInstanceOperation::ModifyInstanceOperation(ServerContext& context,
+                                                   RequestOrigin origin,
+                                                   DicomModification* modification) :
+    context_(context),
+    origin_(origin),
+    modification_(modification)
+  {
+    if (modification == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    
+    modification_->SetAllowManualIdentifiers(true);
+
+    if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID))
+    {
+      modification_->SetLevel(ResourceType_Patient);
+    }
+    else if (modification_->IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+    {
+      modification_->SetLevel(ResourceType_Study);
+    }
+    else if (modification_->IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+    {
+      modification_->SetLevel(ResourceType_Series);
+    }
+    else
+    {
+      modification_->SetLevel(ResourceType_Instance);
+    }
+
+    if (origin_ != RequestOrigin_Lua)
+    {
+      // TODO If issued from HTTP, "remoteIp" and "username" must be provided
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+  void ModifyInstanceOperation::Apply(JobOperationValues& outputs,
+                                      const JobOperationValue& input,
+                                      IDicomConnectionManager& connectionManager)
+  {
+    if (input.GetType() != JobOperationValue::Type_DicomInstance)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    const DicomInstanceOperationValue& instance =
+      dynamic_cast<const DicomInstanceOperationValue&>(input);
+
+    LOG(INFO) << "Lua: Modifying instance " << instance.GetId();
+
+    std::auto_ptr<ParsedDicomFile> modified;
+    
+    {
+      ServerContext::DicomCacheLocker lock(context_, instance.GetId());
+      modified.reset(lock.GetDicom().Clone(true));
+    }
+
+    try
+    {
+      modification_->Apply(*modified);
+
+      DicomInstanceToStore toStore;
+      assert(origin_ == RequestOrigin_Lua);
+      toStore.SetOrigin(DicomInstanceOrigin::FromLua());
+      toStore.SetParsedDicomFile(*modified);
+
+      // TODO other metadata
+      toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, instance.GetId());
+
+      std::string modifiedId;
+      context_.Store(modifiedId, toStore);
+
+      // Only chain with other commands if this command succeeds
+      outputs.Append(new DicomInstanceOperationValue(instance.GetServerContext(), modifiedId));
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Lua: Unable to modify instance " << instance.GetId()
+                 << ": " << e.What();
+    }
+  }
+
+
+  void ModifyInstanceOperation::Serialize(Json::Value& target) const
+  {
+    target = Json::objectValue;
+    target["Type"] = "ModifyInstance";
+    target["Origin"] = EnumerationToString(origin_);
+    modification_->Serialize(target["Modification"]);
+  }
+
+
+  ModifyInstanceOperation::ModifyInstanceOperation(ServerContext& context,
+                                                   const Json::Value& serialized) :
+    context_(context)
+  {
+    if (SerializationToolbox::ReadString(serialized, "Type") != "ModifyInstance" ||
+        !serialized.isMember("Modification"))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    origin_ = StringToRequestOrigin(SerializationToolbox::ReadString(serialized, "Origin"));
+
+    modification_.reset(new DicomModification(serialized["Modification"]));
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../Core/JobsEngine/Operations/IJobOperation.h"
+#include "../../../Core/DicomParsing/DicomModification.h"
+
+#include "../../ServerContext.h"
+
+namespace Orthanc
+{
+  class ModifyInstanceOperation : public IJobOperation
+  {
+  private:
+    ServerContext&                    context_;
+    RequestOrigin                     origin_;
+    std::auto_ptr<DicomModification>  modification_;
+    
+  public:
+    ModifyInstanceOperation(ServerContext& context,
+                            RequestOrigin origin,
+                            DicomModification* modification);  // Takes ownership
+
+    ModifyInstanceOperation(ServerContext& context,
+                            const Json::Value& serialized);
+
+    const RequestOrigin& GetRequestOrigin() const
+    {
+      return origin_;
+    }
+    
+    const DicomModification& GetModification() const
+    {
+      return *modification_;
+    }
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& connectionManager);
+
+    virtual void Serialize(Json::Value& target) const;
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,106 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeadersServer.h"
+#include "StorePeerOperation.h"
+
+#include "DicomInstanceOperationValue.h"
+
+#include "../../../Core/Logging.h"
+#include "../../../Core/OrthancException.h"
+#include "../../../Core/HttpClient.h"
+#include "../../../Core/SerializationToolbox.h"
+
+namespace Orthanc
+{
+  void StorePeerOperation::Apply(JobOperationValues& outputs,
+                                 const JobOperationValue& input,
+                                 IDicomConnectionManager& connectionManager)
+  {
+    // Configure the HTTP client
+    HttpClient client(peer_, "instances");
+    client.SetMethod(HttpMethod_Post);
+
+    if (input.GetType() != JobOperationValue::Type_DicomInstance)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    const DicomInstanceOperationValue& instance =
+      dynamic_cast<const DicomInstanceOperationValue&>(input);
+
+    LOG(INFO) << "Lua: Sending instance " << instance.GetId() << " to Orthanc peer \"" 
+              << peer_.GetUrl() << "\"";
+
+    try
+    {
+      instance.ReadDicom(client.GetBody());
+
+      std::string answer;
+      if (!client.Apply(answer))
+      {
+        LOG(ERROR) << "Lua: Unable to send instance " << instance.GetId()
+                   << " to Orthanc peer \"" << peer_.GetUrl();
+      }
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Lua: Unable to send instance " << instance.GetId()
+                 << " to Orthanc peer \"" << peer_.GetUrl() << "\": " << e.What();
+    }
+
+    outputs.Append(input.Clone());
+  }
+
+  
+  void StorePeerOperation::Serialize(Json::Value& result) const
+  {
+    result = Json::objectValue;
+    result["Type"] = "StorePeer";
+    peer_.Serialize(result["Peer"], 
+                    true /* force advanced format */,
+                    true /* include passwords */);
+  }
+
+
+  StorePeerOperation::StorePeerOperation(const Json::Value& serialized)
+  {
+    if (SerializationToolbox::ReadString(serialized, "Type") != "StorePeer" ||
+        !serialized.isMember("Peer"))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    peer_ = WebServiceParameters(serialized["Peer"]);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/StorePeerOperation.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../Core/JobsEngine/Operations/IJobOperation.h"
+#include "../../../Core/WebServiceParameters.h"
+
+namespace Orthanc
+{
+  class StorePeerOperation : public IJobOperation
+  {
+  private:
+    WebServiceParameters peer_;
+
+  public:
+    StorePeerOperation(const WebServiceParameters& peer) :
+    peer_(peer)
+    {
+    }
+
+    StorePeerOperation(const Json::Value& serialized);
+
+    const WebServiceParameters& GetPeer() const
+    {
+      return peer_;
+    }
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& connectionManager);
+
+    virtual void Serialize(Json::Value& result) const;
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,105 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeadersServer.h"
+#include "StoreScuOperation.h"
+
+#include "DicomInstanceOperationValue.h"
+
+#include "../../../Core/Logging.h"
+#include "../../../Core/OrthancException.h"
+#include "../../../Core/SerializationToolbox.h"
+
+namespace Orthanc
+{
+  void StoreScuOperation::Apply(JobOperationValues& outputs,
+                                const JobOperationValue& input,
+                                IDicomConnectionManager& connectionManager)
+  {
+    std::auto_ptr<IDicomConnectionManager::IResource> resource
+      (connectionManager.AcquireConnection(localAet_, modality_));
+
+    if (resource.get() == NULL)
+    {
+      LOG(ERROR) << "Lua: Cannot connect to modality: " << modality_.GetApplicationEntityTitle();
+      return;
+    }
+
+    if (input.GetType() != JobOperationValue::Type_DicomInstance)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    const DicomInstanceOperationValue& instance =
+      dynamic_cast<const DicomInstanceOperationValue&>(input);
+
+    LOG(INFO) << "Lua: Sending instance " << instance.GetId() << " to modality \"" 
+              << modality_.GetApplicationEntityTitle() << "\"";
+
+    try
+    {
+      std::string dicom;
+      instance.ReadDicom(dicom);
+      resource->GetConnection().Store(dicom);
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Lua: Unable to send instance " << instance.GetId() << " to modality \"" 
+                 << modality_.GetApplicationEntityTitle() << "\": " << e.What();
+    }
+
+    outputs.Append(input.Clone());
+  }
+
+  
+  void StoreScuOperation::Serialize(Json::Value& result) const
+  {
+    result = Json::objectValue;
+    result["Type"] = "StoreScu";
+    result["LocalAET"] = localAet_;
+    modality_.Serialize(result["Modality"], true /* force advanced format */);
+  }
+
+
+  StoreScuOperation::StoreScuOperation(const Json::Value& serialized)
+  {
+    if (SerializationToolbox::ReadString(serialized, "Type") != "StoreScu" ||
+        !serialized.isMember("LocalAET"))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    localAet_ = SerializationToolbox::ReadString(serialized, "LocalAET");
+    modality_ = RemoteModalityParameters(serialized["Modality"]);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../Core/JobsEngine/Operations/IJobOperation.h"
+#include "../../../Core/DicomNetworking/RemoteModalityParameters.h"
+
+namespace Orthanc
+{
+  class StoreScuOperation : public IJobOperation
+  {
+  private:
+    std::string               localAet_;
+    RemoteModalityParameters  modality_;
+    
+  public:
+    StoreScuOperation(const std::string& localAet,
+                      const RemoteModalityParameters& modality) :
+      localAet_(localAet),
+      modality_(modality)
+    {
+    }
+
+    StoreScuOperation(const Json::Value& serialized);
+
+    const std::string& GetLocalAet() const
+    {
+      return localAet_;
+    }
+
+    const RemoteModalityParameters& GetRemoteModality() const
+    {
+      return modality_;
+    }
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& manager);
+
+    virtual void Serialize(Json::Value& result) const;
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,165 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeadersServer.h"
+#include "SystemCallOperation.h"
+
+#include "DicomInstanceOperationValue.h"
+
+#include "../../../Core/JobsEngine/Operations/StringOperationValue.h"
+#include "../../../Core/Logging.h"
+#include "../../../Core/OrthancException.h"
+#include "../../../Core/SerializationToolbox.h"
+#include "../../../Core/TemporaryFile.h"
+#include "../../../Core/Toolbox.h"
+
+namespace Orthanc
+{
+  const std::string& SystemCallOperation::GetPreArgument(size_t i) const
+  {
+    if (i >= preArguments_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return preArguments_[i];
+    }
+  }
+
+  
+  const std::string& SystemCallOperation::GetPostArgument(size_t i) const
+  {
+    if (i >= postArguments_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return postArguments_[i];
+    }
+  }
+
+
+  void SystemCallOperation::Apply(JobOperationValues& outputs,
+                                  const JobOperationValue& input,
+                                  IDicomConnectionManager& connectionManager)
+  {
+    std::vector<std::string> arguments = preArguments_;
+
+    arguments.reserve(arguments.size() + postArguments_.size() + 1);
+
+    std::auto_ptr<TemporaryFile> tmp;
+    
+    switch (input.GetType())
+    {
+      case JobOperationValue::Type_DicomInstance:
+      {
+        const DicomInstanceOperationValue& instance =
+          dynamic_cast<const DicomInstanceOperationValue&>(input);
+
+        std::string dicom;
+        instance.ReadDicom(dicom);
+
+        tmp.reset(new TemporaryFile);
+        tmp->Write(dicom);
+        
+        arguments.push_back(tmp->GetPath());
+        break;
+      }
+
+      case JobOperationValue::Type_String:
+      {
+        const StringOperationValue& value =
+          dynamic_cast<const StringOperationValue&>(input);
+
+        arguments.push_back(value.GetContent());
+        break;
+      }
+
+      case JobOperationValue::Type_Null:
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    for (size_t i = 0; i < postArguments_.size(); i++)
+    {
+      arguments.push_back(postArguments_[i]);
+    }
+
+    std::string info = command_;
+    for (size_t i = 0; i < arguments.size(); i++)
+    {
+      info += " " + arguments[i];
+    }
+    
+    LOG(INFO) << "Lua: System call: \"" << info << "\"";
+
+    try
+    {
+      SystemToolbox::ExecuteSystemCommand(command_, arguments);
+
+      // Only chain with other commands if this operation succeeds
+      outputs.Append(input.Clone());
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Lua: Failed system call - \"" << info << "\": " << e.What();
+    }
+  }
+
+
+  void SystemCallOperation::Serialize(Json::Value& result) const
+  {
+    result = Json::objectValue;
+    result["Type"] = "SystemCall";
+    result["Command"] = command_;
+    SerializationToolbox::WriteArrayOfStrings(result, preArguments_, "PreArguments");
+    SerializationToolbox::WriteArrayOfStrings(result, postArguments_, "PostArguments");
+  }
+
+
+  SystemCallOperation::SystemCallOperation(const Json::Value& serialized)
+  {
+    if (SerializationToolbox::ReadString(serialized, "Type") != "SystemCall")
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    command_ = SerializationToolbox::ReadString(serialized, "Command");
+    SerializationToolbox::ReadArrayOfStrings(preArguments_, serialized, "PreArguments");
+    SerializationToolbox::ReadArrayOfStrings(postArguments_, serialized, "PostArguments");
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/SystemCallOperation.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,102 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../Core/JobsEngine/Operations/IJobOperation.h"
+
+#include <string>
+
+namespace Orthanc
+{
+  class SystemCallOperation : public IJobOperation
+  {
+  private:
+    std::string               command_;
+    std::vector<std::string>  preArguments_;
+    std::vector<std::string>  postArguments_;
+    
+  public:
+    SystemCallOperation(const std::string& command) :
+      command_(command)
+    {
+    }
+
+    SystemCallOperation(const Json::Value& serialized);
+
+    SystemCallOperation(const std::string& command,
+                        const std::vector<std::string>& preArguments,
+                        const std::vector<std::string>& postArguments) :
+      command_(command),
+      preArguments_(preArguments),
+      postArguments_(postArguments)
+    {
+    }
+
+    void AddPreArgument(const std::string& argument)
+    {
+      preArguments_.push_back(argument);
+    }
+
+    void AddPostArgument(const std::string& argument)
+    {
+      postArguments_.push_back(argument);
+    }
+
+    const std::string& GetCommand() const
+    {
+      return command_;
+    }
+
+    size_t GetPreArgumentsCount() const
+    {
+      return preArguments_.size();
+    }
+
+    size_t GetPostArgumentsCount() const
+    {
+      return postArguments_.size();
+    }
+
+    const std::string& GetPreArgument(size_t i) const;
+
+    const std::string& GetPostArgument(size_t i) const;
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& connectionManager);
+
+    virtual void Serialize(Json::Value& result) const;
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,147 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "OrthancJobUnserializer.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/SerializationToolbox.h"
+
+#include "Operations/DeleteResourceOperation.h"
+#include "Operations/DicomInstanceOperationValue.h"
+#include "Operations/ModifyInstanceOperation.h"
+#include "Operations/StorePeerOperation.h"
+#include "Operations/StoreScuOperation.h"
+#include "Operations/SystemCallOperation.h"
+
+#include "DicomModalityStoreJob.h"
+#include "DicomMoveScuJob.h"
+#include "OrthancPeerStoreJob.h"
+#include "ResourceModificationJob.h"
+#include "MergeStudyJob.h"
+#include "SplitStudyJob.h"
+
+namespace Orthanc
+{
+  IJob* OrthancJobUnserializer::UnserializeJob(const Json::Value& source)
+  {
+    const std::string type = SerializationToolbox::ReadString(source, "Type");
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (context_.HasPlugins())
+    {
+      std::auto_ptr<IJob> job(context_.GetPlugins().UnserializeJob(type, source));
+      if (job.get() != NULL)
+      {
+        return job.release();
+      }
+    }
+#endif
+
+    if (type == "DicomModalityStore")
+    {
+      return new DicomModalityStoreJob(context_, source);
+    }
+    else if (type == "OrthancPeerStore")
+    {
+      return new OrthancPeerStoreJob(context_, source);
+    }
+    else if (type == "ResourceModification")
+    {
+      return new ResourceModificationJob(context_, source);
+    }
+    else if (type == "MergeStudy")
+    {
+      return new MergeStudyJob(context_, source);
+    }
+    else if (type == "SplitStudy")
+    {
+      return new SplitStudyJob(context_, source);
+    }
+    else if (type == "DicomMoveScu")
+    {
+      return new DicomMoveScuJob(context_, source);
+    }
+    else
+    {
+      return GenericJobUnserializer::UnserializeJob(source);
+    }
+  }
+
+
+  IJobOperation* OrthancJobUnserializer::UnserializeOperation(const Json::Value& source)
+  {
+    const std::string type = SerializationToolbox::ReadString(source, "Type");
+
+    if (type == "DeleteResource")
+    {
+      return new DeleteResourceOperation(context_);
+    }
+    else if (type == "ModifyInstance")
+    {
+      return new ModifyInstanceOperation(context_, source);
+    }
+    else if (type == "StorePeer")
+    {
+      return new StorePeerOperation(source);
+    }
+    else if (type == "StoreScu")
+    {
+      return new StoreScuOperation(source);
+    }
+    else if (type == "SystemCall")
+    {
+      return new SystemCallOperation(source);
+    }
+    else
+    {
+      return GenericJobUnserializer::UnserializeOperation(source);
+    }
+  }
+
+
+  JobOperationValue* OrthancJobUnserializer::UnserializeValue(const Json::Value& source)
+  {
+    const std::string type = SerializationToolbox::ReadString(source, "Type");
+
+    if (type == "DicomInstance")
+    {
+      return new DicomInstanceOperationValue(context_, SerializationToolbox::ReadString(source, "ID"));
+    }
+    else
+    {
+      return GenericJobUnserializer::UnserializeValue(source);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../ServerContext.h"
+#include "../../Core/JobsEngine/GenericJobUnserializer.h"
+
+namespace Orthanc
+{
+  class OrthancJobUnserializer : public GenericJobUnserializer
+  {
+  private:
+    ServerContext&  context_;
+
+  public:
+    OrthancJobUnserializer(ServerContext& context) :
+      context_(context)
+    {
+    }
+
+    virtual IJob* UnserializeJob(const Json::Value& value);
+
+    virtual IJobOperation* UnserializeOperation(const Json::Value& value);
+
+    virtual JobOperationValue* UnserializeValue(const Json::Value& value);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,139 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "OrthancPeerStoreJob.h"
+
+#include "../../Core/Logging.h"
+
+
+namespace Orthanc
+{
+  bool OrthancPeerStoreJob::HandleInstance(const std::string& instance)
+  {
+    //boost::this_thread::sleep(boost::posix_time::milliseconds(500));
+
+    if (client_.get() == NULL)
+    {
+      client_.reset(new HttpClient(peer_, "instances"));
+      client_->SetMethod(HttpMethod_Post);
+    }
+      
+    LOG(INFO) << "Sending instance " << instance << " to peer \"" 
+              << peer_.GetUrl() << "\"";
+
+    try
+    {
+      context_.ReadDicom(client_->GetBody(), instance);
+    }
+    catch (OrthancException& e)
+    {
+      LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
+      return false;
+    }
+
+    std::string answer;
+    if (client_->Apply(answer))
+    {
+      return true;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NetworkProtocol);
+    }
+  }
+    
+
+  bool OrthancPeerStoreJob::HandleTrailingStep()
+  {
+    throw OrthancException(ErrorCode_InternalError);
+  }
+
+
+  void OrthancPeerStoreJob::SetPeer(const WebServiceParameters& peer)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      peer_ = peer;
+    }
+  }
+
+
+  void OrthancPeerStoreJob::Stop(JobStopReason reason)   // For pausing jobs
+  {
+    client_.reset(NULL);
+  }
+
+
+  void OrthancPeerStoreJob::GetPublicContent(Json::Value& value)
+  {
+    SetOfInstancesJob::GetPublicContent(value);
+    
+    Json::Value v;
+    peer_.Serialize(v, 
+                    false /* allow simple format if possible */,
+                    false /* don't include passwords */);
+    value["Peer"] = v;
+  }
+
+
+  static const char* PEER = "Peer";
+
+  OrthancPeerStoreJob::OrthancPeerStoreJob(ServerContext& context,
+                                           const Json::Value& serialized) :
+    SetOfInstancesJob(serialized),
+    context_(context)
+  {
+    peer_ = WebServiceParameters(serialized[PEER]);
+  }
+
+
+  bool OrthancPeerStoreJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfInstancesJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      peer_.Serialize(target[PEER],
+                      true /* force advanced format */,
+                      true /* include passwords */);
+      return true;
+    }
+  }  
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/JobsEngine/SetOfInstancesJob.h"
+#include "../../Core/HttpClient.h"
+
+#include "../ServerContext.h"
+
+
+namespace Orthanc
+{
+  class OrthancPeerStoreJob : public SetOfInstancesJob
+  {
+  private:
+    ServerContext&             context_;
+    WebServiceParameters       peer_;
+    std::auto_ptr<HttpClient>  client_;
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance);
+    
+    virtual bool HandleTrailingStep();
+
+  public:
+    OrthancPeerStoreJob(ServerContext& context) :
+      context_(context)
+    {
+    }
+
+    OrthancPeerStoreJob(ServerContext& context,
+                        const Json::Value& serialize);
+
+    void SetPeer(const WebServiceParameters& peer);
+
+    const WebServiceParameters& GetPeer() const
+    {
+      return peer_;
+    }
+
+    virtual void Stop(JobStopReason reason);   // For pausing jobs
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = "OrthancPeerStore";
+    }
+
+    virtual void GetPublicContent(Json::Value& value);
+
+    virtual bool Serialize(Json::Value& target);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/ResourceModificationJob.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,332 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "ResourceModificationJob.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/SerializationToolbox.h"
+
+namespace Orthanc
+{
+  class ResourceModificationJob::Output : public boost::noncopyable
+  {
+  private:
+    ResourceType  level_;
+    bool          isFirst_;
+    std::string   id_;
+    std::string   patientId_;
+
+  public:
+    Output(ResourceType level) :
+      level_(level),
+      isFirst_(true)
+    {
+      if (level_ != ResourceType_Patient &&
+          level_ != ResourceType_Study &&
+          level_ != ResourceType_Series)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }            
+    }
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+    
+
+    void Update(DicomInstanceHasher& hasher)
+    {
+      if (isFirst_)
+      {
+        switch (level_)
+        {
+          case ResourceType_Series:
+            id_ = hasher.HashSeries();
+            break;
+
+          case ResourceType_Study:
+            id_ = hasher.HashStudy();
+            break;
+
+          case ResourceType_Patient:
+            id_ = hasher.HashPatient();
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+
+        patientId_ = hasher.HashPatient();
+        isFirst_ = false;
+      }
+    }
+
+
+    bool Format(Json::Value& target)
+    {
+      if (isFirst_)
+      {
+        return false;
+      }
+      else
+      {
+        target = Json::objectValue;
+        target["Type"] = EnumerationToString(level_);
+        target["ID"] = id_;
+        target["Path"] = GetBasePath(level_, id_);
+        target["PatientID"] = patientId_;
+        return true;
+      }
+    }
+
+  
+    bool GetIdentifier(std::string& id)
+    {
+      if (isFirst_)
+      {
+        return false;
+      }
+      else
+      {
+        id = id_;
+        return true;
+      }
+    }
+  };
+    
+
+
+
+  bool ResourceModificationJob::HandleInstance(const std::string& instance)
+  {
+    if (modification_.get() == NULL ||
+        output_.get() == NULL)
+    {
+      LOG(ERROR) << "No modification was provided for this job";
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+      
+    LOG(INFO) << "Modifying instance in a job: " << instance;
+
+
+    /**
+     * Retrieve the original instance from the DICOM cache.
+     **/
+    
+    std::auto_ptr<DicomInstanceHasher> originalHasher;
+    std::auto_ptr<ParsedDicomFile> modified;
+
+    try
+    {
+      ServerContext::DicomCacheLocker locker(context_, instance);
+      ParsedDicomFile& original = locker.GetDicom();
+
+      originalHasher.reset(new DicomInstanceHasher(original.GetHasher()));
+      modified.reset(original.Clone(true));
+    }
+    catch (OrthancException&)
+    {
+      LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
+      return false;
+    }
+
+
+    /**
+     * Compute the resulting DICOM instance.
+     **/
+
+    modification_->Apply(*modified);
+
+    DicomInstanceToStore toStore;
+    toStore.SetOrigin(origin_);
+    toStore.SetParsedDicomFile(*modified);
+
+
+    /**
+     * Prepare the metadata information to associate with the
+     * resulting DICOM instance (AnonymizedFrom/ModifiedFrom).
+     **/
+
+    DicomInstanceHasher modifiedHasher = modified->GetHasher();
+      
+    MetadataType metadataType = (isAnonymization_ ?
+                                 MetadataType_AnonymizedFrom :
+                                 MetadataType_ModifiedFrom);
+
+    if (originalHasher->HashSeries() != modifiedHasher.HashSeries())
+    {
+      toStore.AddMetadata(ResourceType_Series, metadataType, originalHasher->HashSeries());
+    }
+
+    if (originalHasher->HashStudy() != modifiedHasher.HashStudy())
+    {
+      toStore.AddMetadata(ResourceType_Study, metadataType, originalHasher->HashStudy());
+    }
+
+    if (originalHasher->HashPatient() != modifiedHasher.HashPatient())
+    {
+      toStore.AddMetadata(ResourceType_Patient, metadataType, originalHasher->HashPatient());
+    }
+
+    assert(instance == originalHasher->HashInstance());
+    toStore.AddMetadata(ResourceType_Instance, metadataType, instance);
+
+
+    /**
+     * Store the resulting DICOM instance into the Orthanc store.
+     **/
+
+    std::string modifiedInstance;
+    if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success)
+    {
+      LOG(ERROR) << "Error while storing a modified instance " << instance;
+      throw OrthancException(ErrorCode_CannotStoreInstance);
+    }
+
+    assert(modifiedInstance == modifiedHasher.HashInstance());
+
+    output_->Update(modifiedHasher);
+
+    return true;
+  }
+
+  
+  bool ResourceModificationJob::HandleTrailingStep()
+  {
+    throw OrthancException(ErrorCode_InternalError);
+  }
+
+
+  void ResourceModificationJob::SetModification(DicomModification* modification,
+                                                ResourceType level,
+                                                bool isAnonymization)
+  {
+    if (modification == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      modification_.reset(modification);
+      output_.reset(new Output(level));
+      isAnonymization_ = isAnonymization;
+    }
+  }
+
+
+  void ResourceModificationJob::SetOrigin(const DicomInstanceOrigin& origin)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      origin_ = origin;
+    }
+  }
+
+  
+  void ResourceModificationJob::SetOrigin(const RestApiCall& call)
+  {
+    SetOrigin(DicomInstanceOrigin::FromRest(call));
+  }
+
+
+  const DicomModification& ResourceModificationJob::GetModification() const
+  {
+    if (modification_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *modification_;
+    }
+  }
+
+
+  void ResourceModificationJob::GetPublicContent(Json::Value& value)
+  {
+    SetOfInstancesJob::GetPublicContent(value);
+
+    value["IsAnonymization"] = isAnonymization_;
+
+    if (output_.get() != NULL)
+    {
+      output_->Format(value);
+    }
+  }
+
+
+  static const char* MODIFICATION = "Modification";
+  static const char* ORIGIN = "Origin";
+  static const char* IS_ANONYMIZATION = "IsAnonymization";
+  
+
+  ResourceModificationJob::ResourceModificationJob(ServerContext& context,
+                                                   const Json::Value& serialized) :
+    SetOfInstancesJob(serialized),
+    context_(context)
+  {
+    isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION);
+    origin_ = DicomInstanceOrigin(serialized[ORIGIN]);
+    modification_.reset(new DicomModification(serialized[MODIFICATION]));
+  }
+  
+  bool ResourceModificationJob::Serialize(Json::Value& value)
+  {
+    if (!SetOfInstancesJob::Serialize(value))
+    {
+      return false;
+    }
+    else
+    {
+      value[IS_ANONYMIZATION] = isAnonymization_;
+      origin_.Serialize(value[ORIGIN]);
+      
+      Json::Value tmp;
+      modification_->Serialize(tmp);
+      value[MODIFICATION] = tmp;
+
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/ResourceModificationJob.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,100 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/JobsEngine/SetOfInstancesJob.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class ResourceModificationJob : public SetOfInstancesJob
+  {
+  private:
+    class Output;
+    
+    ServerContext&                    context_;
+    std::auto_ptr<DicomModification>  modification_;
+    boost::shared_ptr<Output>         output_;
+    bool                              isAnonymization_;
+    DicomInstanceOrigin               origin_;
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance);
+    
+    virtual bool HandleTrailingStep();
+
+  public:
+    ResourceModificationJob(ServerContext& context) :
+      context_(context),
+      isAnonymization_(false)
+    {
+    }
+
+    ResourceModificationJob(ServerContext& context,
+                            const Json::Value& serialized);
+
+    void SetModification(DicomModification* modification,   // Takes ownership
+                         ResourceType level,
+                         bool isAnonymization);
+
+    void SetOrigin(const DicomInstanceOrigin& origin);
+
+    void SetOrigin(const RestApiCall& call);
+
+    const DicomModification& GetModification() const;
+
+    bool IsAnonymization() const
+    {
+      return isAnonymization_;
+    }
+
+    const DicomInstanceOrigin& GetOrigin() const
+    {
+      return origin_;
+    }
+
+    virtual void Stop(JobStopReason reason)
+    {
+    }
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = "ResourceModification";
+    }
+
+    virtual void GetPublicContent(Json::Value& value);
+    
+    virtual bool Serialize(Json::Value& value);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/SplitStudyJob.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,370 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "SplitStudyJob.h"
+
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/Logging.h"
+#include "../../Core/SerializationToolbox.h"
+
+namespace Orthanc
+{
+  void SplitStudyJob::CheckAllowedTag(const DicomTag& tag) const
+  {
+    if (allowedTags_.find(tag) == allowedTags_.end())
+    {
+      LOG(ERROR) << "Cannot modify the following tag while splitting a study "
+                 << "(not in the patient/study modules): "
+                 << FromDcmtkBridge::GetTagName(tag, "") << " (" << tag.Format() << ")";
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  
+  void SplitStudyJob::Setup()
+  {
+    SetPermissive(false);
+    
+    DicomTag::AddTagsForModule(allowedTags_, DicomModule_Patient);
+    DicomTag::AddTagsForModule(allowedTags_, DicomModule_Study);
+    allowedTags_.erase(DICOM_TAG_STUDY_INSTANCE_UID);
+    allowedTags_.erase(DICOM_TAG_SERIES_INSTANCE_UID);
+  }
+
+  
+  bool SplitStudyJob::HandleInstance(const std::string& instance)
+  {
+    /**
+     * Retrieve the DICOM instance to be modified
+     **/
+    
+    std::auto_ptr<ParsedDicomFile> modified;
+
+    try
+    {
+      ServerContext::DicomCacheLocker locker(context_, instance);
+      modified.reset(locker.GetDicom().Clone(true));
+    }
+    catch (OrthancException&)
+    {
+      LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
+      return false;
+    }
+
+
+    /**
+     * Chose the target UIDs
+     **/
+
+    assert(modified->GetHasher().HashStudy() == sourceStudy_);
+
+    std::string series = modified->GetHasher().HashSeries();
+
+    SeriesUidMap::const_iterator targetSeriesUid = seriesUidMap_.find(series);
+
+    if (targetSeriesUid == seriesUidMap_.end())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);  // Should never happen
+    }
+
+
+    /**
+     * Apply user-specified modifications
+     **/
+
+    for (std::set<DicomTag>::const_iterator it = removals_.begin();
+         it != removals_.end(); ++it)
+    {
+      modified->Remove(*it);
+    }
+    
+    for (Replacements::const_iterator it = replacements_.begin();
+         it != replacements_.end(); ++it)
+    {
+      modified->ReplacePlainString(it->first, it->second);
+    }
+
+
+    /**
+     * Store the new instance into Orthanc
+     **/
+    
+    modified->ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, targetStudyUid_);
+    modified->ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, targetSeriesUid->second);
+
+    if (targetStudy_.empty())
+    {
+      targetStudy_ = modified->GetHasher().HashStudy();
+    }
+    
+    DicomInstanceToStore toStore;
+    toStore.SetOrigin(origin_);
+    toStore.SetParsedDicomFile(*modified);
+
+    std::string modifiedInstance;
+    if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success)
+    {
+      LOG(ERROR) << "Error while storing a modified instance " << instance;
+      return false;
+    }
+
+    return true;
+  }
+
+  
+  bool SplitStudyJob::HandleTrailingStep()
+  {
+    if (!keepSource_)
+    {
+      const size_t n = GetInstancesCount();
+
+      for (size_t i = 0; i < n; i++)
+      {
+        Json::Value tmp;
+        context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance);
+      }
+    }
+
+    return true;
+  }
+
+  
+  SplitStudyJob::SplitStudyJob(ServerContext& context,
+                               const std::string& sourceStudy) :
+    context_(context),
+    keepSource_(false),
+    sourceStudy_(sourceStudy),
+    targetStudyUid_(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study))
+  {
+    Setup();
+    
+    ResourceType type;
+    
+    if (!context_.GetIndex().LookupResourceType(type, sourceStudy) ||
+        type != ResourceType_Study)
+    {
+      LOG(ERROR) << "Cannot split unknown study: " << sourceStudy;
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+  
+
+  void SplitStudyJob::SetOrigin(const DicomInstanceOrigin& origin)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      origin_ = origin;
+    }
+  }
+
+  
+  void SplitStudyJob::SetOrigin(const RestApiCall& call)
+  {
+    SetOrigin(DicomInstanceOrigin::FromRest(call));
+  }
+
+
+  void SplitStudyJob::AddSourceSeries(const std::string& series)
+  {
+    std::string parent;
+
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study) ||
+             parent != sourceStudy_)
+    {
+      LOG(ERROR) << "This series does not belong to the study to be split: " << series;
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+    else
+    {
+      // Generate a target SeriesInstanceUID for this series
+      seriesUidMap_[series] = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series);
+
+      // Add all the instances of the series as to be processed
+      std::list<std::string> instances;
+      context_.GetIndex().GetChildren(instances, series);
+
+      for (std::list<std::string>::const_iterator
+             it = instances.begin(); it != instances.end(); ++it)
+      {
+        AddInstance(*it);
+      }
+    }    
+  }
+
+
+  void SplitStudyJob::SetKeepSource(bool keep)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    keepSource_ = keep;
+  }
+
+
+  bool SplitStudyJob::LookupTargetSeriesUid(std::string& uid,
+                                            const std::string& series) const
+  {
+    SeriesUidMap::const_iterator found = seriesUidMap_.find(series);
+
+    if (found == seriesUidMap_.end())
+    {
+      return false;
+    }
+    else
+    {
+      uid = found->second;
+      return true;
+    }
+  }
+
+
+  void SplitStudyJob::Remove(const DicomTag& tag)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    CheckAllowedTag(tag);
+    removals_.insert(tag);
+  }
+
+  
+  void SplitStudyJob::Replace(const DicomTag& tag,
+                              const std::string& value)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    CheckAllowedTag(tag);
+    replacements_[tag] = value;
+  }
+
+
+  bool SplitStudyJob::LookupReplacement(std::string& value,
+                                        const DicomTag& tag) const
+  {
+    Replacements::const_iterator found = replacements_.find(tag);
+
+    if (found == replacements_.end())
+    {
+      return false;
+    }
+    else
+    {
+      value = found->second;
+      return true;
+    }
+  }
+  
+    
+  void SplitStudyJob::GetPublicContent(Json::Value& value)
+  {
+    SetOfInstancesJob::GetPublicContent(value);
+
+    if (!targetStudy_.empty())
+    {
+      value["TargetStudy"] = targetStudy_;
+    }
+    
+    value["TargetStudyUID"] = targetStudyUid_;
+  }
+
+
+  static const char* KEEP_SOURCE = "KeepSource";
+  static const char* SOURCE_STUDY = "SourceStudy";
+  static const char* TARGET_STUDY = "TargetStudy";
+  static const char* TARGET_STUDY_UID = "TargetStudyUID";
+  static const char* SERIES_UID_MAP = "SeriesUIDMap";
+  static const char* ORIGIN = "Origin";
+  static const char* REPLACEMENTS = "Replacements";
+  static const char* REMOVALS = "Removals";
+
+
+  SplitStudyJob::SplitStudyJob(ServerContext& context,
+                               const Json::Value& serialized) :
+    SetOfInstancesJob(serialized),  // (*)
+    context_(context)
+  {
+    if (!HasTrailingStep())
+    {
+      // Should have been set by (*)
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    Setup();
+
+    keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE);
+    sourceStudy_ = SerializationToolbox::ReadString(serialized, SOURCE_STUDY);
+    targetStudy_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY);
+    targetStudyUid_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY_UID);
+    SerializationToolbox::ReadMapOfStrings(seriesUidMap_, serialized, SERIES_UID_MAP);
+    origin_ = DicomInstanceOrigin(serialized[ORIGIN]);
+    SerializationToolbox::ReadMapOfTags(replacements_, serialized, REPLACEMENTS);
+    SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS);
+  }
+
+  
+  bool SplitStudyJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfInstancesJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      target[KEEP_SOURCE] = keepSource_;
+      target[SOURCE_STUDY] = sourceStudy_;
+      target[TARGET_STUDY] = targetStudy_;
+      target[TARGET_STUDY_UID] = targetStudyUid_;
+      SerializationToolbox::WriteMapOfStrings(target, seriesUidMap_, SERIES_UID_MAP);
+      origin_.Serialize(target[ORIGIN]);
+      SerializationToolbox::WriteMapOfTags(target, replacements_, REPLACEMENTS);
+      SerializationToolbox::WriteSetOfTags(target, removals_, REMOVALS);
+
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/SplitStudyJob.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,138 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/JobsEngine/SetOfInstancesJob.h"
+
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class SplitStudyJob : public SetOfInstancesJob
+  {
+  private:
+    typedef std::map<std::string, std::string>  SeriesUidMap;
+    typedef std::map<DicomTag, std::string>     Replacements;
+    
+    
+    ServerContext&         context_;
+    std::set<DicomTag>     allowedTags_;
+    bool                   keepSource_;
+    std::string            sourceStudy_;
+    std::string            targetStudy_;
+    std::string            targetStudyUid_;
+    SeriesUidMap           seriesUidMap_;
+    DicomInstanceOrigin    origin_;
+    Replacements           replacements_;
+    std::set<DicomTag>     removals_;
+
+    void CheckAllowedTag(const DicomTag& tag) const;
+    
+    void Setup();
+    
+  protected:
+    virtual bool HandleInstance(const std::string& instance);
+
+    virtual bool HandleTrailingStep();
+    
+  public:
+    SplitStudyJob(ServerContext& context,
+                  const std::string& sourceStudy);
+
+    SplitStudyJob(ServerContext& context,
+                  const Json::Value& serialized);
+
+    const std::string& GetSourceStudy() const
+    {
+      return sourceStudy_;
+    }
+
+    const std::string& GetTargetStudy() const
+    {
+      return targetStudy_;
+    }
+
+    const std::string& GetTargetStudyUid() const
+    {
+      return targetStudyUid_;
+    }
+
+    void AddSourceSeries(const std::string& series);
+
+    bool IsKeepSource() const
+    {
+      return keepSource_;
+    }
+    
+    void SetKeepSource(bool keep);
+
+    bool LookupTargetSeriesUid(std::string& uid,
+                               const std::string& series) const;
+
+    void Replace(const DicomTag& tag,
+                 const std::string& value);
+    
+    bool LookupReplacement(std::string& value,
+                           const DicomTag& tag) const;
+
+    void Remove(const DicomTag& tag);
+    
+    bool IsRemoved(const DicomTag& tag) const
+    {
+      return removals_.find(tag) != removals_.end();
+    }
+
+    void SetOrigin(const DicomInstanceOrigin& origin);
+
+    void SetOrigin(const RestApiCall& call);
+
+    const DicomInstanceOrigin& GetOrigin() const
+    {
+      return origin_;
+    }
+
+    virtual void Stop(JobStopReason reason)
+    {
+    }
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = "SplitStudy";
+    }
+
+    virtual void GetPublicContent(Json::Value& value);
+
+    virtual bool Serialize(Json::Value& target);
+  };
+}
--- a/OrthancServer/ServerToolbox.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/ServerToolbox.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -37,17 +38,45 @@
 #include "../Core/FileStorage/StorageAccessor.h"
 #include "../Core/Logging.h"
 #include "../Core/OrthancException.h"
-#include "ParsedDicomFile.h"
-#include "Search/LookupIdentifierQuery.h"
 
 #include <cassert>
 
 namespace Orthanc
 {
-  namespace Toolbox
+  namespace ServerToolbox
   {
+    static const DicomTag patientIdentifiers[] = 
+    {
+      DICOM_TAG_PATIENT_ID,
+      DICOM_TAG_PATIENT_NAME,
+      DICOM_TAG_PATIENT_BIRTH_DATE
+    };
+
+    static const DicomTag studyIdentifiers[] = 
+    {
+      DICOM_TAG_PATIENT_ID,
+      DICOM_TAG_PATIENT_NAME,
+      DICOM_TAG_PATIENT_BIRTH_DATE,
+      DICOM_TAG_STUDY_INSTANCE_UID,
+      DICOM_TAG_ACCESSION_NUMBER,
+      DICOM_TAG_STUDY_DESCRIPTION,
+      DICOM_TAG_STUDY_DATE
+    };
+
+    static const DicomTag seriesIdentifiers[] = 
+    {
+      DICOM_TAG_SERIES_INSTANCE_UID
+    };
+
+    static const DicomTag instanceIdentifiers[] = 
+    {
+      DICOM_TAG_SOP_INSTANCE_UID
+    };
+
+
     void SimplifyTags(Json::Value& target,
-                      const Json::Value& source)
+                      const Json::Value& source,
+                      DicomToJsonFormat format)
     {
       assert(source.isObject());
 
@@ -57,9 +86,23 @@
       for (size_t i = 0; i < members.size(); i++)
       {
         const Json::Value& v = source[members[i]];
-        const std::string& name = v["Name"].asString();
         const std::string& type = v["Type"].asString();
 
+        std::string name;
+        switch (format)
+        {
+          case DicomToJsonFormat_Human:
+            name = v["Name"].asString();
+            break;
+
+          case DicomToJsonFormat_Short:
+            name = members[i];
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+
         if (type == "String")
         {
           target[name] = v["Value"].asString();
@@ -78,7 +121,7 @@
           for (Json::Value::ArrayIndex i = 0; i < array.size(); i++)
           {
             Json::Value c;
-            SimplifyTags(c, array[i]);
+            SimplifyTags(c, array[i], format);
             children.append(c);
           }
 
@@ -92,91 +135,9 @@
     }
 
 
-    static std::string ValueAsString(const DicomMap& summary,
-                                     const DicomTag& tag)
-    {
-      const DicomValue& value = summary.GetValue(tag);
-      if (value.IsNull())
-      {
-        return "(null)";
-      }
-      else
-      {
-        return value.GetContent();
-      }
-    }
-
-
-    void LogMissingRequiredTag(const DicomMap& summary)
-    {
-      std::string s, t;
-
-      if (summary.HasTag(DICOM_TAG_PATIENT_ID))
-      {
-        if (t.size() > 0)
-          t += ", ";
-        t += "PatientID=" + ValueAsString(summary, DICOM_TAG_PATIENT_ID);
-      }
-      else
-      {
-        if (s.size() > 0)
-          s += ", ";
-        s += "PatientID";
-      }
-
-      if (summary.HasTag(DICOM_TAG_STUDY_INSTANCE_UID))
-      {
-        if (t.size() > 0)
-          t += ", ";
-        t += "StudyInstanceUID=" + ValueAsString(summary, DICOM_TAG_STUDY_INSTANCE_UID);
-      }
-      else
-      {
-        if (s.size() > 0)
-          s += ", ";
-        s += "StudyInstanceUID";
-      }
-
-      if (summary.HasTag(DICOM_TAG_SERIES_INSTANCE_UID))
-      {
-        if (t.size() > 0)
-          t += ", ";
-        t += "SeriesInstanceUID=" + ValueAsString(summary, DICOM_TAG_SERIES_INSTANCE_UID);
-      }
-      else
-      {
-        if (s.size() > 0)
-          s += ", ";
-        s += "SeriesInstanceUID";
-      }
-
-      if (summary.HasTag(DICOM_TAG_SOP_INSTANCE_UID))
-      {
-        if (t.size() > 0)
-          t += ", ";
-        t += "SOPInstanceUID=" + ValueAsString(summary, DICOM_TAG_SOP_INSTANCE_UID);
-      }
-      else
-      {
-        if (s.size() > 0)
-          s += ", ";
-        s += "SOPInstanceUID";
-      }
-
-      if (t.size() == 0)
-      {
-        LOG(ERROR) << "Store has failed because all the required tags (" << s << ") are missing (is it a DICOMDIR file?)";
-      }
-      else
-      {
-        LOG(ERROR) << "Store has failed because required tags (" << s << ") are missing for the following instance: " << t;
-      }
-    }
-
-
-    static void SetMainDicomTagsInternal(IDatabaseWrapper& database,
-                                         int64_t resource,
-                                         const DicomMap& tags)
+    static void StoreMainDicomTagsInternal(IDatabaseWrapper& database,
+                                           int64_t resource,
+                                           const DicomMap& tags)
     {
       DicomArray flattened(tags);
 
@@ -194,14 +155,38 @@
     }
 
 
-    void SetMainDicomTags(IDatabaseWrapper& database,
-                          int64_t resource,
-                          ResourceType level,
-                          const DicomMap& dicomSummary)
+    static void StoreIdentifiers(IDatabaseWrapper& database,
+                                 int64_t resource,
+                                 ResourceType level,
+                                 const DicomMap& map)
+    {
+      const DicomTag* tags;
+      size_t size;
+
+      LoadIdentifiers(tags, size, level);
+
+      for (size_t i = 0; i < size; i++)
+      {
+        const DicomValue* value = map.TestAndGetValue(tags[i]);
+        if (value != NULL &&
+            !value->IsNull() &&
+            !value->IsBinary())
+        {
+          std::string s = NormalizeIdentifier(value->GetContent());
+          database.SetIdentifierTag(resource, tags[i], s);
+        }
+      }
+    }
+
+
+    void StoreMainDicomTags(IDatabaseWrapper& database,
+                            int64_t resource,
+                            ResourceType level,
+                            const DicomMap& dicomSummary)
     {
       // WARNING: The database should be locked with a transaction!
 
-      LookupIdentifierQuery::StoreIdentifiers(database, resource, level, dicomSummary);
+      StoreIdentifiers(database, resource, level, dicomSummary);
 
       DicomMap tags;
 
@@ -214,7 +199,7 @@
         case ResourceType_Study:
           // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6)
           dicomSummary.ExtractPatientInformation(tags);
-          SetMainDicomTagsInternal(database, resource, tags);
+          StoreMainDicomTagsInternal(database, resource, tags);
 
           dicomSummary.ExtractStudyInformation(tags);
           break;
@@ -231,7 +216,7 @@
           throw OrthancException(ErrorCode_InternalError);
       }
 
-      SetMainDicomTagsInternal(database, resource, tags);
+      StoreMainDicomTagsInternal(database, resource, tags);
     }
 
 
@@ -267,6 +252,13 @@
     {
       // WARNING: The database should be locked with a transaction!
 
+      // TODO: This function might consume much memory if level ==
+      // ResourceType_Instance. To improve this, first download the
+      // list of studies, then remove the instances for each single
+      // study (check out OrthancRestApi::InvalidateTags for an
+      // example). Take this improvement into consideration for the
+      // next upgrade of the database schema.
+
       const char* plural = NULL;
 
       switch (level)
@@ -297,7 +289,7 @@
       database.GetAllPublicIds(resources, level);
 
       for (std::list<std::string>::const_iterator
-             it = resources.begin(); it != resources.end(); it++)
+             it = resources.begin(); it != resources.end(); ++it)
       {
         // Locate the resource and one of its child instances
         int64_t resource, instance;
@@ -307,6 +299,8 @@
             tmp != level ||
             !FindOneChildInstance(instance, database, resource, level))
         {
+          LOG(ERROR) << "Cannot find an instance for " << EnumerationToString(level) 
+                     << " with identifier " << *it;
           throw OrthancException(ErrorCode_InternalError);
         }
 
@@ -314,23 +308,135 @@
         FileInfo attachment;
         if (!database.LookupAttachment(attachment, instance, FileContentType_Dicom))
         {
+          LOG(ERROR) << "Cannot retrieve the DICOM file associated with instance " << database.GetPublicId(instance);
           throw OrthancException(ErrorCode_InternalError);
         }
 
-        // Read and parse the content of the DICOM file
-        StorageAccessor accessor(storageArea);
+        try
+        {
+          // Read and parse the content of the DICOM file
+          StorageAccessor accessor(storageArea);
+
+          std::string content;
+          accessor.Read(content, attachment);
+
+          ParsedDicomFile dicom(content);
+
+          // Update the tags of this resource
+          DicomMap dicomSummary;
+          dicom.ExtractDicomSummary(dicomSummary);
+
+          database.ClearMainDicomTags(resource);
+          StoreMainDicomTags(database, resource, level, dicomSummary);
+        }
+        catch (OrthancException&)
+        {
+          LOG(ERROR) << "Cannot decode the DICOM file with UUID " << attachment.GetUuid()
+                     << " associated with instance " << database.GetPublicId(instance);
+          throw;
+        }
+      }
+    }
+
 
-        std::string content;
-        accessor.Read(content, attachment);
+    void LoadIdentifiers(const DicomTag*& tags,
+                         size_t& size,
+                         ResourceType level)
+    {
+      switch (level)
+      {
+        case ResourceType_Patient:
+          tags = patientIdentifiers;
+          size = sizeof(patientIdentifiers) / sizeof(DicomTag);
+          break;
+
+        case ResourceType_Study:
+          tags = studyIdentifiers;
+          size = sizeof(studyIdentifiers) / sizeof(DicomTag);
+          break;
+
+        case ResourceType_Series:
+          tags = seriesIdentifiers;
+          size = sizeof(seriesIdentifiers) / sizeof(DicomTag);
+          break;
+
+        case ResourceType_Instance:
+          tags = instanceIdentifiers;
+          size = sizeof(instanceIdentifiers) / sizeof(DicomTag);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
 
-        ParsedDicomFile dicom(content);
+    std::string NormalizeIdentifier(const std::string& value)
+    {
+      std::string t;
+      t.reserve(value.size());
+
+      for (size_t i = 0; i < value.size(); i++)
+      {
+        if (value[i] == '%' ||
+            value[i] == '_')
+        {
+          t.push_back(' ');  // These characters might break wildcard queries in SQL
+        }
+        else if (isascii(value[i]) &&
+                 !iscntrl(value[i]) &&
+                 (!isspace(value[i]) || value[i] == ' '))
+        {
+          t.push_back(value[i]);
+        }
+      }
+
+      Toolbox::ToUpperCase(t);
+
+      return Toolbox::StripSpaces(t);
+    }
+
+
+    bool IsIdentifier(const DicomTag& tag,
+                      ResourceType level)
+    {
+      const DicomTag* tags;
+      size_t size;
 
-        // Update the tags of this resource
-        DicomMap dicomSummary;
-        dicom.Convert(dicomSummary);
+      LoadIdentifiers(tags, size, level);
+
+      for (size_t i = 0; i < size; i++)
+      {
+        if (tag == tags[i])
+        {
+          return true;
+        }
+      }
+
+      return false;
+    }
 
-        database.ClearMainDicomTags(resource);
-        Toolbox::SetMainDicomTags(database, resource, level, dicomSummary);
+    
+    void ReconstructResource(ServerContext& context,
+                             const std::string& resource)
+    {
+      LOG(WARNING) << "Reconstructing resource " << resource;
+      
+      std::list<std::string> instances;
+      context.GetIndex().GetChildInstances(instances, resource);
+
+      for (std::list<std::string>::const_iterator 
+             it = instances.begin(); it != instances.end(); ++it)
+      {
+        ServerContext::DicomCacheLocker locker(context, *it);
+
+        Json::Value dicomAsJson;
+        locker.GetDicom().DatasetToJson(dicomAsJson);
+
+        std::string s = dicomAsJson.toStyledString();
+        context.AddAttachment(*it, FileContentType_DicomAsJson, s.c_str(), s.size());
+
+        context.GetIndex().ReconstructInstance(locker.GetDicom());
       }
     }
   }
--- a/OrthancServer/ServerToolbox.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/ServerToolbox.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,24 +33,22 @@
 
 #pragma once
 
-#include "../Core/DicomFormat/DicomMap.h"
-#include "IDatabaseWrapper.h"
+#include "ServerContext.h"
 
 #include <json/json.h>
 
 namespace Orthanc
 {
-  namespace Toolbox
+  namespace ServerToolbox
   {
     void SimplifyTags(Json::Value& target,
-                      const Json::Value& source);
-
-    void LogMissingRequiredTag(const DicomMap& summary);
+                      const Json::Value& source,
+                      DicomToJsonFormat format);
 
-    void SetMainDicomTags(IDatabaseWrapper& database,
-                          int64_t resource,
-                          ResourceType level,
-                          const DicomMap& dicomSummary);
+    void StoreMainDicomTags(IDatabaseWrapper& database,
+                            int64_t resource,
+                            ResourceType level,
+                            const DicomMap& dicomSummary);
 
     bool FindOneChildInstance(int64_t& result,
                               IDatabaseWrapper& database,
@@ -59,5 +58,17 @@
     void ReconstructMainDicomTags(IDatabaseWrapper& database,
                                   IStorageArea& storageArea,
                                   ResourceType level);
+
+    void LoadIdentifiers(const DicomTag*& tags,
+                         size_t& size,
+                         ResourceType level);
+
+    bool IsIdentifier(const DicomTag& tag,
+                      ResourceType level);
+
+    std::string NormalizeIdentifier(const std::string& value);
+
+    void ReconstructResource(ServerContext& context,
+                             const std::string& resource);
   }
 }
--- a/OrthancServer/SliceOrdering.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/SliceOrdering.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -94,12 +95,69 @@
   }
 
 
+  static bool IsCloseToZero(double x)
+  {
+    return fabs(x) < 10.0 * std::numeric_limits<float>::epsilon();
+  }
+
+  
+  bool SliceOrdering::ComputeNormal(Vector& normal,
+                                    const DicomMap& dicom)
+  {
+    std::vector<float> cosines;
+
+    if (TokenizeVector(cosines, dicom, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6))
+    {
+      assert(cosines.size() == 6);
+      normal[0] = cosines[1] * cosines[5] - cosines[2] * cosines[4];
+      normal[1] = cosines[2] * cosines[3] - cosines[0] * cosines[5];
+      normal[2] = cosines[0] * cosines[4] - cosines[1] * cosines[3];
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool SliceOrdering::IsParallelOrOpposite(const Vector& u,
+                                           const Vector& v)
+  {
+    // Check out "GeometryToolbox::IsParallelOrOpposite()" in Stone of
+    // Orthanc for explanations
+    const double u1 = u[0];
+    const double u2 = u[1];
+    const double u3 = u[2];
+    const double normU = sqrt(u1 * u1 + u2 * u2 + u3 * u3);
+
+    const double v1 = v[0];
+    const double v2 = v[1];
+    const double v3 = v[2];
+    const double normV = sqrt(v1 * v1 + v2 * v2 + v3 * v3);
+
+    if (IsCloseToZero(normU * normV))
+    {
+      return false;
+    }
+    else
+    {
+      const double cosAngle = (u1 * v1 + u2 * v2 + u3 * v3) / (normU * normV);
+
+      return (IsCloseToZero(cosAngle - 1.0) ||      // Close to +1: Parallel, non-opposite
+              IsCloseToZero(fabs(cosAngle) - 1.0)); // Close to -1: Parallel, opposite
+    }
+  }
+
+  
   struct SliceOrdering::Instance : public boost::noncopyable
   {
   private:
     std::string   instanceId_;
     bool          hasPosition_;
     Vector        position_;   
+    bool          hasNormal_;
+    Vector        normal_;   
     bool          hasIndexInSeries_;
     size_t        indexInSeries_;
     unsigned int  framesCount_;
@@ -140,6 +198,8 @@
         position_[2] = tmp[2];
       }
 
+      hasNormal_ = ComputeNormal(normal_, instance);
+
       std::string s;
       hasIndexInSeries_ = false;
 
@@ -189,6 +249,17 @@
     {
       return framesCount_;
     }
+
+    bool HasNormal() const
+    {
+      return hasNormal_;
+    }
+
+    const Vector& GetNormal() const
+    {
+      assert(hasNormal_);
+      return normal_;
+    }
   };
 
 
@@ -225,15 +296,7 @@
       throw OrthancException(ErrorCode_UnknownResource);
     }
 
-    std::vector<float> cosines;
-    hasNormal_ = TokenizeVector(cosines, series, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6);
-
-    if (hasNormal_)
-    {
-      normal_[0] = cosines[1] * cosines[5] - cosines[2] * cosines[4];
-      normal_[1] = cosines[2] * cosines[3] - cosines[0] * cosines[5];
-      normal_[2] = cosines[0] * cosines[4] - cosines[1] * cosines[3];
-    }
+    hasNormal_ = ComputeNormal(normal_, series);
   }
 
 
@@ -267,7 +330,10 @@
     for (size_t i = 0; i < instances_.size(); i++)
     {
       assert(instances_[i] != NULL);
-      if (!instances_[i]->HasPosition())
+
+      if (!instances_[i]->HasPosition() ||
+          (instances_[i]->HasNormal() &&
+           !IsParallelOrOpposite(instances_[i]->GetNormal(), normal_)))
       {
         return false;
       }
@@ -276,21 +342,23 @@
     PositionComparator comparator(normal_);
     std::sort(instances_.begin(), instances_.end(), comparator);
 
-    float a = instances_.front()->ComputeRelativePosition(normal_);
-    float b = instances_.back()->ComputeRelativePosition(normal_);
-
-    if (std::fabs(b - a) <= 10.0f * std::numeric_limits<float>::epsilon())
+    float a = instances_[0]->ComputeRelativePosition(normal_);
+    for (size_t i = 1; i < instances_.size(); i++)
     {
-      // Not enough difference between the minimum and maximum
-      // positions along the normal of the volume
-      return false;
+      float b = instances_[i]->ComputeRelativePosition(normal_);
+
+      if (std::fabs(b - a) <= 10.0f * std::numeric_limits<float>::epsilon())
+      {
+        // Not enough space between two slices along the normal of the volume
+        return false;
+      }
+
+      a = b;
     }
-    else
-    {
-      // This is a 3D volume
-      isVolume_ = true;
-      return true;
-    }
+
+    // This is a 3D volume
+    isVolume_ = true;
+    return true;
   }
 
 
@@ -318,7 +386,8 @@
       if (instances_[i - 1]->GetIndexInSeries() == instances_[i]->GetIndexInSeries())
       {
         // The current "IndexInSeries" occurs 2 times: Not a proper ordering
-        return false;
+        LOG(WARNING) << "This series contains 2 slices with the same index, trying to display it anyway";
+        break;
       }
     }
 
@@ -396,6 +465,8 @@
 
     result["Dicom"] = tmp;
 
+    Json::Value slicesShort = Json::arrayValue;
+
     tmp.clear();
     for (size_t i = 0; i < GetInstancesCount(); i++)
     {
@@ -404,8 +475,16 @@
       {
         tmp.append(base + "/frames/" + boost::lexical_cast<std::string>(j));
       }
+
+      Json::Value tmp2 = Json::arrayValue;
+      tmp2.append(GetInstanceId(i));
+      tmp2.append(0);
+      tmp2.append(GetFramesCount(i));
+      
+      slicesShort.append(tmp2);
     }
 
     result["Slices"] = tmp;
+    result["SlicesShort"] = slicesShort;
   }
 }
--- a/OrthancServer/SliceOrdering.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/SliceOrdering.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -42,7 +43,7 @@
     typedef float Vector[3];
 
     struct Instance;
-    struct PositionComparator;
+    class  PositionComparator;
 
     ServerIndex&             index_;
     std::string              seriesId_;
@@ -51,6 +52,12 @@
     std::vector<Instance*>   instances_;
     bool                     isVolume_;
 
+    static bool ComputeNormal(Vector& normal,
+                              const DicomMap& dicom);
+
+    static bool IsParallelOrOpposite(const Vector& a,
+                                     const Vector& b);
+
     static bool IndexInSeriesComparator(const SliceOrdering::Instance* a,
                                         const SliceOrdering::Instance* b);
 
--- a/OrthancServer/ToDcmtkBridge.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "PrecompiledHeadersServer.h"
-#include "ToDcmtkBridge.h"
-
-#include <memory>
-#include <dcmtk/dcmnet/diutil.h>
-
-
-namespace Orthanc
-{
-  DcmTagKey ToDcmtkBridge::Convert(const DicomTag& tag)
-  {
-    return DcmTagKey(tag.GetGroup(), tag.GetElement());
-  }
-
-
-  DcmDataset* ToDcmtkBridge::Convert(const DicomMap& map)
-  {
-    std::auto_ptr<DcmDataset> result(new DcmDataset);
-
-    for (DicomMap::Map::const_iterator 
-           it = map.map_.begin(); it != map.map_.end(); ++it)
-    {
-      if (!it->second->IsNull())
-      {
-        std::string s = it->second->GetContent();
-        DU_putStringDOElement(result.get(), Convert(it->first), s.c_str());
-      }
-    }
-
-    return result.release();
-  }
-}
--- a/OrthancServer/ToDcmtkBridge.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,47 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../Core/DicomFormat/DicomMap.h"
-#include <dcmtk/dcmdata/dcdatset.h>
-
-namespace Orthanc
-{
-  class ToDcmtkBridge
-  {
-  public:
-    static DcmTagKey Convert(const DicomTag& tag);
-
-    static DcmDataset* Convert(const DicomMap& map);
-  };
-}
--- a/OrthancServer/main.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/OrthancServer/main.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,24 +34,21 @@
 #include "PrecompiledHeadersServer.h"
 #include "OrthancRestApi/OrthancRestApi.h"
 
-#include <fstream>
 #include <boost/algorithm/string/predicate.hpp>
 
 #include "../Core/Logging.h"
-#include "../Core/Uuid.h"
 #include "../Core/HttpServer/EmbeddedResourceHttpHandler.h"
 #include "../Core/HttpServer/FilesystemHttpHandler.h"
 #include "../Core/Lua/LuaFunctionCall.h"
 #include "../Core/DicomFormat/DicomArray.h"
-#include "DicomProtocol/DicomServer.h"
-#include "DicomProtocol/ReusableDicomUserConnection.h"
+#include "../Core/DicomNetworking/DicomServer.h"
 #include "OrthancInitialization.h"
 #include "ServerContext.h"
 #include "OrthancFindRequestHandler.h"
 #include "OrthancMoveRequestHandler.h"
 #include "ServerToolbox.h"
 #include "../Plugins/Engine/OrthancPlugins.h"
-#include "FromDcmtkBridge.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
 
 using namespace Orthanc;
 
@@ -77,7 +75,8 @@
     if (dicomFile.size() > 0)
     {
       DicomInstanceToStore toStore;
-      toStore.SetDicomProtocolOrigin(remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str());
+      toStore.SetOrigin(DicomInstanceOrigin::FromDicomProtocol
+                        (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str()));
       toStore.SetBuffer(dicomFile);
       toStore.SetSummary(dicomSummary);
       toStore.SetJson(dicomJson);
@@ -90,6 +89,23 @@
 
 
 
+class ModalitiesFromConfiguration : public Orthanc::DicomServer::IRemoteModalities
+{
+public:
+  virtual bool IsSameAETitle(const std::string& aet1,
+                             const std::string& aet2) 
+  {
+    return Orthanc::Configuration::IsSameAETitle(aet1, aet2);
+  }
+
+  virtual bool LookupAETitle(RemoteModalityParameters& modality,
+                             const std::string& aet) 
+  {
+    return Orthanc::Configuration::LookupDicomModalityUsingAETitle(modality, aet);
+  }
+};
+
+
 class MyDicomServerFactory : 
   public IStoreRequestHandlerFactory,
   public IFindRequestHandlerFactory, 
@@ -112,8 +128,8 @@
   {
     std::auto_ptr<OrthancFindRequestHandler> result(new OrthancFindRequestHandler(context_));
 
-    result->SetMaxResults(Configuration::GetGlobalIntegerParameter("LimitFindResults", 0));
-    result->SetMaxInstances(Configuration::GetGlobalIntegerParameter("LimitFindInstances", 0));
+    result->SetMaxResults(Configuration::GetGlobalUnsignedIntegerParameter("LimitFindResults", 0));
+    result->SetMaxInstances(Configuration::GetGlobalUnsignedIntegerParameter("LimitFindInstances", 0));
 
     if (result->GetMaxResults() == 0)
     {
@@ -152,42 +168,68 @@
 class OrthancApplicationEntityFilter : public IApplicationEntityFilter
 {
 private:
-  ServerContext& context_;
+  ServerContext&  context_;
+  bool            alwaysAllowEcho_;
+  bool            alwaysAllowStore_;
 
 public:
-  OrthancApplicationEntityFilter(ServerContext& context) : context_(context)
+  OrthancApplicationEntityFilter(ServerContext& context) :
+    context_(context)
   {
-  }
-
-  virtual bool IsAllowedConnection(const std::string& /*callingIp*/,
-                                   const std::string& /*callingAet*/)
-  {
-    return true;
+    alwaysAllowEcho_ = Configuration::GetGlobalBoolParameter("DicomAlwaysAllowEcho", true);
+    alwaysAllowStore_ = Configuration::GetGlobalBoolParameter("DicomAlwaysAllowStore", true);
   }
 
-  virtual bool IsAllowedRequest(const std::string& /*callingIp*/,
-                                const std::string& callingAet,
+  virtual bool IsAllowedConnection(const std::string& remoteIp,
+                                   const std::string& remoteAet,
+                                   const std::string& calledAet)
+  {
+    LOG(INFO) << "Incoming connection from AET " << remoteAet
+              << " on IP " << remoteIp << ", calling AET " << calledAet;
+
+    return (alwaysAllowEcho_ ||
+            alwaysAllowStore_ ||
+            Configuration::IsKnownAETitle(remoteAet, remoteIp));
+  }
+
+  virtual bool IsAllowedRequest(const std::string& remoteIp,
+                                const std::string& remoteAet,
+                                const std::string& calledAet,
                                 DicomRequestType type)
   {
-    if (type == DicomRequestType_Store)
+    LOG(INFO) << "Incoming " << Orthanc::EnumerationToString(type) << " request from AET "
+              << remoteAet << " on IP " << remoteIp << ", calling AET " << calledAet;
+    
+    if (type == DicomRequestType_Echo &&
+        alwaysAllowEcho_)
     {
-      // Incoming store requests are always accepted, even from unknown AET
+      // Incoming C-Echo requests are always accepted, even from unknown AET
       return true;
     }
-
-    if (!Configuration::IsKnownAETitle(callingAet))
+    else if (type == DicomRequestType_Store &&
+             alwaysAllowStore_)
     {
-      LOG(ERROR) << "Unknown remote DICOM modality AET: \"" << callingAet << "\"";
-      return false;
+      // Incoming C-Store requests are always accepted, even from unknown AET
+      return true;
     }
     else
     {
-      return true;
+      RemoteModalityParameters modality;
+    
+      if (Configuration::LookupDicomModalityUsingAETitle(modality, remoteAet))
+      {
+        return modality.IsRequestAllowed(type);
+      }
+      else
+      {
+        return false;
+      }
     }
   }
 
-  virtual bool IsAllowedTransferSyntax(const std::string& callingIp,
-                                       const std::string& callingAet,
+  virtual bool IsAllowedTransferSyntax(const std::string& remoteIp,
+                                       const std::string& remoteAet,
+                                       const std::string& calledAet,
                                        TransferSyntax syntax)
   {
     std::string configuration;
@@ -227,47 +269,85 @@
     }
 
     {
-      std::string lua = "Is" + configuration;
+      std::string name = "Is" + configuration;
 
-      LuaScripting::Locker locker(context_.GetLua());
+      LuaScripting::Lock lock(context_.GetLuaScripting());
       
-      if (locker.GetLua().IsExistingFunction(lua.c_str()))
+      if (lock.GetLua().IsExistingFunction(name.c_str()))
       {
-        LuaFunctionCall call(locker.GetLua(), lua.c_str());
-        call.PushString(callingAet);
-        call.PushString(callingIp);
+        LuaFunctionCall call(lock.GetLua(), name.c_str());
+        call.PushString(remoteAet);
+        call.PushString(remoteIp);
+        call.PushString(calledAet);
         return call.ExecutePredicate();
       }
     }
 
     return Configuration::GetGlobalBoolParameter(configuration, true);
   }
+
+
+  virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
+                                         const std::string& remoteAet,
+                                         const std::string& calledAet)
+  {
+    static const char* configuration = "UnknownSopClassAccepted";
+
+    {
+      std::string lua = "Is" + std::string(configuration);
+
+      LuaScripting::Lock lock(context_.GetLuaScripting());
+      
+      if (lock.GetLua().IsExistingFunction(lua.c_str()))
+      {
+        LuaFunctionCall call(lock.GetLua(), lua.c_str());
+        call.PushString(remoteAet);
+        call.PushString(remoteIp);
+        call.PushString(calledAet);
+        return call.ExecutePredicate();
+      }
+    }
+
+    return Configuration::GetGlobalBoolParameter(configuration, false);
+  }
 };
 
 
 class MyIncomingHttpRequestFilter : public IIncomingHttpRequestFilter
 {
 private:
-  ServerContext& context_;
+  ServerContext&   context_;
+  OrthancPlugins*  plugins_;
 
 public:
-  MyIncomingHttpRequestFilter(ServerContext& context) : context_(context)
+  MyIncomingHttpRequestFilter(ServerContext& context,
+                              OrthancPlugins* plugins) : 
+    context_(context),
+    plugins_(plugins)
   {
   }
 
   virtual bool IsAllowed(HttpMethod method,
                          const char* uri,
                          const char* ip,
-                         const char* username) const
+                         const char* username,
+                         const IHttpHandler::Arguments& httpHeaders,
+                         const IHttpHandler::GetArguments& getArguments)
   {
+    if (plugins_ != NULL &&
+        !plugins_->IsAllowed(method, uri, ip, username, httpHeaders, getArguments))
+    {
+      return false;
+    }
+
     static const char* HTTP_FILTER = "IncomingHttpRequestFilter";
 
-    LuaScripting::Locker locker(context_.GetLua());
+    LuaScripting::Lock lock(context_.GetLuaScripting());
 
     // Test if the instance must be filtered out
-    if (locker.GetLua().IsExistingFunction(HTTP_FILTER))
+    if (lock.GetLua().IsExistingFunction(HTTP_FILTER))
     {
-      LuaFunctionCall call(locker.GetLua(), HTTP_FILTER);
+      LuaFunctionCall call(lock.GetLua(), HTTP_FILTER);
 
       switch (method)
       {
@@ -294,6 +374,7 @@
       call.PushString(uri);
       call.PushString(ip);
       call.PushString(username);
+      call.PushStringMap(httpHeaders);
 
       if (!call.ExecutePredicate())
       {
@@ -330,7 +411,7 @@
     {
       bool isPlugin = false;
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
       if (plugins_ != NULL)
       {
         plugins_->GetErrorDictionary().LogError(exception.GetErrorCode(), true);
@@ -351,7 +432,7 @@
     {
       bool isPlugin = false;
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
       if (plugins_ != NULL &&
           plugins_->GetErrorDictionary().Format(message, httpStatus, exception))
       {
@@ -402,7 +483,9 @@
     << "Command-line options:" << std::endl
     << "  --help\t\tdisplay this help and exit" << std::endl
     << "  --logdir=[dir]\tdirectory where to store the log files" << std::endl
-    << "\t\t\t(if not used, the logs are dumped to stderr)" << std::endl
+    << "\t\t\t(by default, the log is dumped to stderr)" << std::endl
+    << "  --logfile=[file]\tfile where to store the log of Orthanc" << std::endl
+    << "\t\t\t(by default, the log is dumped to stderr)" << std::endl
     << "  --config=[file]\tcreate a sample configuration file and exit" << std::endl
     << "  --errors\t\tprint the supported error codes and exit" << std::endl
     << "  --verbose\t\tbe verbose in logs" << std::endl
@@ -410,6 +493,8 @@
     << "  --upgrade\t\tallow Orthanc to upgrade the version of the" << std::endl
     << "\t\t\tdatabase (beware that the database will become" << std::endl
     << "\t\t\tincompatible with former versions of Orthanc)" << std::endl
+    << "  --no-jobs\t\tDon't restart the jobs that were stored during" << std::endl
+    << "\t\t\tthe last execution of Orthanc" << std::endl
     << "  --version\t\toutput version information and exit" << std::endl
     << std::endl
     << "Exit status:" << std::endl
@@ -427,7 +512,8 @@
 {
   std::cout
     << path << " " << ORTHANC_VERSION << std::endl
-    << "Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics Department, University Hospital of Liege (Belgium)" << std::endl
+    << "Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics Department, University Hospital of Liege (Belgium)" << std::endl
+    << "Copyright (C) 2017-2018 Osimis S.A. (Belgium)" << std::endl
     << "Licensing GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>, with OpenSSL exception." << std::endl
     << "This is free software: you are free to change and redistribute it." << std::endl
     << "There is NO WARRANTY, to the extent permitted by law." << std::endl
@@ -462,7 +548,7 @@
     PrintErrorCode(ErrorCode_Plugin, "Error encountered within the plugin engine");
     PrintErrorCode(ErrorCode_NotImplemented, "Not implemented yet");
     PrintErrorCode(ErrorCode_ParameterOutOfRange, "Parameter out of range");
-    PrintErrorCode(ErrorCode_NotEnoughMemory, "Not enough memory");
+    PrintErrorCode(ErrorCode_NotEnoughMemory, "The server hosting Orthanc is running out of memory");
     PrintErrorCode(ErrorCode_BadParameterType, "Bad type for a parameter");
     PrintErrorCode(ErrorCode_BadSequenceOfCalls, "Bad sequence of calls");
     PrintErrorCode(ErrorCode_InexistentItem, "Accessing an inexistent item");
@@ -492,6 +578,10 @@
     PrintErrorCode(ErrorCode_DatabasePlugin, "The plugin implementing a custom database back-end does not fulfill the proper interface");
     PrintErrorCode(ErrorCode_StorageAreaPlugin, "Error in the plugin implementing a custom storage area");
     PrintErrorCode(ErrorCode_EmptyRequest, "The request is empty");
+    PrintErrorCode(ErrorCode_NotAcceptable, "Cannot send a response which is acceptable according to the Accept HTTP header");
+    PrintErrorCode(ErrorCode_NullPointer, "Cannot handle a NULL pointer");
+    PrintErrorCode(ErrorCode_DatabaseUnavailable, "The database is currently not available (probably a transient situation)");
+    PrintErrorCode(ErrorCode_CanceledJob, "This job was canceled");
     PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened");
     PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open");
     PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database");
@@ -511,8 +601,8 @@
     PrintErrorCode(ErrorCode_DirectoryOverFile, "The directory to be created is already occupied by a regular file");
     PrintErrorCode(ErrorCode_FileStorageCannotWrite, "Unable to create a subdirectory or a file in the file storage");
     PrintErrorCode(ErrorCode_DirectoryExpected, "The specified path does not point to a directory");
-    PrintErrorCode(ErrorCode_HttpPortInUse, "The TCP port of the HTTP server is already in use");
-    PrintErrorCode(ErrorCode_DicomPortInUse, "The TCP port of the DICOM server is already in use");
+    PrintErrorCode(ErrorCode_HttpPortInUse, "The TCP port of the HTTP server is privileged or already in use");
+    PrintErrorCode(ErrorCode_DicomPortInUse, "The TCP port of the DICOM server is privileged or already in use");
     PrintErrorCode(ErrorCode_BadHttpStatusInRest, "This HTTP status is not allowed in a REST API");
     PrintErrorCode(ErrorCode_RegularFileExpected, "The specified path does not point to a regular file");
     PrintErrorCode(ErrorCode_PathToExecutable, "Unable to get the path to the executable");
@@ -549,6 +639,8 @@
     PrintErrorCode(ErrorCode_DatabaseNotInitialized, "Plugin trying to call the database during its initialization");
     PrintErrorCode(ErrorCode_SslDisabled, "Orthanc has been built without SSL support");
     PrintErrorCode(ErrorCode_CannotOrderSlices, "Unable to order the slices of the series");
+    PrintErrorCode(ErrorCode_NoWorklistHandler, "No request handler factory for DICOM C-Find Modality SCP");
+    PrintErrorCode(ErrorCode_AlreadyExistingTag, "Cannot override the value of a tag that already exists");
   }
 
   std::cout << std::endl;
@@ -556,26 +648,7 @@
 
 
 
-static void LoadLuaScripts(ServerContext& context)
-{
-  std::list<std::string> luaScripts;
-  Configuration::GetGlobalListOfStringsParameter(luaScripts, "LuaScripts");
-  for (std::list<std::string>::const_iterator
-         it = luaScripts.begin(); it != luaScripts.end(); ++it)
-  {
-    std::string path = Configuration::InterpretStringParameterAsPath(*it);
-    LOG(WARNING) << "Installing the Lua scripts from: " << path;
-    std::string script;
-    Toolbox::ReadFile(script, path);
-
-    LuaScripting::Locker locker(context.GetLua());
-    locker.GetLua().Execute(script);
-  }
-}
-
-
-
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
 static void LoadPlugins(OrthancPlugins& plugins)
 {
   std::list<std::string> path;
@@ -598,21 +671,52 @@
 {
   LOG(WARNING) << "Orthanc has started";
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
   if (context.HasPlugins())
   {
     context.GetPlugins().SignalOrthancStarted();
   }
 #endif
 
-  context.GetLua().Execute("Initialize");
+  context.GetLuaScripting().Start();
+  context.GetLuaScripting().Execute("Initialize");
+
+  bool restart;
+
+  for (;;)
+  {
+    ServerBarrierEvent event = SystemToolbox::ServerBarrier(restApi.LeaveBarrierFlag());
+    restart = restApi.IsResetRequestReceived();
+
+    if (!restart && 
+        event == ServerBarrierEvent_Reload)
+    {
+      // Handling of SIGHUP
 
-  Toolbox::ServerBarrier(restApi.LeaveBarrierFlag());
-  bool restart = restApi.IsResetRequestReceived();
+      if (Configuration::HasConfigurationChanged())
+      {
+        LOG(WARNING) << "A SIGHUP signal has been received, resetting Orthanc";
+        Logging::Flush();
+        restart = true;
+        break;
+      }
+      else
+      {
+        LOG(WARNING) << "A SIGHUP signal has been received, but is ignored as the configuration has not changed";
+        Logging::Flush();
+        continue;
+      }
+    }
+    else
+    {
+      break;
+    }
+  }
 
-  context.GetLua().Execute("Finalize");
+  context.GetLuaScripting().Execute("Finalize");
+  context.GetLuaScripting().Stop();
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
   if (context.HasPlugins())
   {
     context.GetPlugins().SignalOrthancStopped();
@@ -646,9 +750,9 @@
   
 
   // HTTP server
-  MyIncomingHttpRequestFilter httpFilter(context);
+  MyIncomingHttpRequestFilter httpFilter(context, plugins);
   MongooseServer httpServer;
-  httpServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("HttpPort", 8042));
+  httpServer.SetPortNumber(Configuration::GetGlobalUnsignedIntegerParameter("HttpPort", 8042));
   httpServer.SetRemoteAccessAllowed(Configuration::GetGlobalBoolParameter("RemoteAccessAllowed", false));
   httpServer.SetKeepAliveEnabled(Configuration::GetGlobalBoolParameter("KeepAlive", false));
   httpServer.SetHttpCompressionEnabled(Configuration::GetGlobalBoolParameter("HttpCompressionEnabled", true));
@@ -672,8 +776,14 @@
 
   httpServer.Register(context.GetHttpHandler());
 
+  if (httpServer.GetPortNumber() < 1024)
+  {
+    LOG(WARNING) << "The HTTP port is privileged (" 
+                 << httpServer.GetPortNumber() << " is below 1024), "
+                 << "make sure you run Orthanc as root/administrator";
+  }
+
   httpServer.Start();
-  LOG(WARNING) << "HTTP server listening on port: " << httpServer.GetPortNumber();
   
   bool restart = WaitForExit(context, restApi);
 
@@ -695,22 +805,55 @@
   }
 
   MyDicomServerFactory serverFactory(context);
-
-  // DICOM server
+  OrthancApplicationEntityFilter dicomFilter(context);
+  ModalitiesFromConfiguration modalities;
+  
+  // Setup the DICOM server  
   DicomServer dicomServer;
-  OrthancApplicationEntityFilter dicomFilter(context);
+  dicomServer.SetRemoteModalities(modalities);
   dicomServer.SetCalledApplicationEntityTitleCheck(Configuration::GetGlobalBoolParameter("DicomCheckCalledAet", false));
   dicomServer.SetStoreRequestHandlerFactory(serverFactory);
   dicomServer.SetMoveRequestHandlerFactory(serverFactory);
   dicomServer.SetFindRequestHandlerFactory(serverFactory);
-  dicomServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("DicomPort", 4242));
+  dicomServer.SetAssociationTimeout(Configuration::GetGlobalUnsignedIntegerParameter("DicomScpTimeout", 30));
+
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+  if (plugins != NULL)
+  {
+    if (plugins->HasWorklistHandler())
+    {
+      dicomServer.SetWorklistRequestHandlerFactory(*plugins);
+    }
+
+    if (plugins->HasFindHandler())
+    {
+      dicomServer.SetFindRequestHandlerFactory(*plugins);
+    }
+
+    if (plugins->HasMoveHandler())
+    {
+      dicomServer.SetMoveRequestHandlerFactory(*plugins);
+    }
+  }
+#endif
+
+  dicomServer.SetPortNumber(Configuration::GetGlobalUnsignedIntegerParameter("DicomPort", 4242));
   dicomServer.SetApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"));
   dicomServer.SetApplicationEntityFilter(dicomFilter);
 
+  if (dicomServer.GetPortNumber() < 1024)
+  {
+    LOG(WARNING) << "The DICOM port is privileged (" 
+                 << dicomServer.GetPortNumber() << " is below 1024), "
+                 << "make sure you run Orthanc as root/administrator";
+  }
+
   dicomServer.Start();
-  LOG(WARNING) << "DICOM server listening on port: " << dicomServer.GetPortNumber();
+  LOG(WARNING) << "DICOM server listening with AET " << dicomServer.GetApplicationEntityTitle() 
+               << " on port: " << dicomServer.GetPortNumber();
 
-  bool restart;
+  bool restart = false;
   ErrorCode error = ErrorCode_Success;
 
   try
@@ -737,9 +880,10 @@
 
 
 static bool ConfigureHttpHandler(ServerContext& context,
-                                 OrthancPlugins *plugins)
+                                 OrthancPlugins *plugins,
+                                 bool loadJobsFromDatabase)
 {
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
   // By order of priority, first apply the "plugins" layer, so that
   // plugins can overwrite the built-in REST API of Orthanc
   if (plugins)
@@ -762,65 +906,95 @@
   OrthancRestApi restApi(context);
   context.GetHttpHandler().Register(restApi, true);
 
-  return StartDicomServer(context, restApi, plugins);
+  context.SetupJobsEngine(false /* not running unit tests */, loadJobsFromDatabase);
+
+  bool restart = StartDicomServer(context, restApi, plugins);
+
+  context.Stop();
+
+  return restart;
 }
 
 
-static bool UpgradeDatabase(IDatabaseWrapper& database,
-                            IStorageArea& storageArea,
-                            bool allowDatabaseUpgrade)
+static void UpgradeDatabase(IDatabaseWrapper& database,
+                            IStorageArea& storageArea)
 {
   // Upgrade the schema of the database, if needed
   unsigned int currentVersion = database.GetDatabaseVersion();
+
+  LOG(WARNING) << "Starting the upgrade of the database schema";
+  LOG(WARNING) << "Current database version: " << currentVersion;
+  LOG(WARNING) << "Database version expected by Orthanc: " << ORTHANC_DATABASE_VERSION;
+  
   if (currentVersion == ORTHANC_DATABASE_VERSION)
   {
-    return true;
+    LOG(WARNING) << "No upgrade is needed, start Orthanc without the \"--upgrade\" argument";
+    return;
   }
 
   if (currentVersion > ORTHANC_DATABASE_VERSION)
   {
     LOG(ERROR) << "The version of the database schema (" << currentVersion
                << ") is too recent for this version of Orthanc. Please upgrade Orthanc.";
-    return false;
-  }
-
-  if (!allowDatabaseUpgrade)
-  {
-    LOG(ERROR) << "The database schema must be upgraded from version "
-               << currentVersion << " to " << ORTHANC_DATABASE_VERSION 
-               << ": Please run Orthanc with the \"--upgrade\" command-line option";
-    return false;
+    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
   }
 
   LOG(WARNING) << "Upgrading the database from schema version "
                << currentVersion << " to " << ORTHANC_DATABASE_VERSION;
-  database.Upgrade(ORTHANC_DATABASE_VERSION, storageArea);
+
+  try
+  {
+    database.Upgrade(ORTHANC_DATABASE_VERSION, storageArea);
+  }
+  catch (OrthancException&)
+  {
+    LOG(ERROR) << "Unable to run the automated upgrade, please use the replication instructions: "
+               << "http://book.orthanc-server.com//users/replication.html";
+    throw;
+  }
     
   // Sanity check
   currentVersion = database.GetDatabaseVersion();
   if (ORTHANC_DATABASE_VERSION != currentVersion)
   {
     LOG(ERROR) << "The database schema was not properly upgraded, it is still at version " << currentVersion;
-    throw OrthancException(ErrorCode_InternalError);
+    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
   }
-
-  return true;
+  else
+  {
+    LOG(WARNING) << "The database schema was successfully upgraded, "
+                 << "you can now start Orthanc without the \"--upgrade\" argument";
+  }
 }
 
 
 static bool ConfigureServerContext(IDatabaseWrapper& database,
                                    IStorageArea& storageArea,
-                                   OrthancPlugins *plugins)
+                                   OrthancPlugins *plugins,
+                                   bool loadJobsFromDatabase)
 {
-  ServerContext context(database, storageArea);
+  // These configuration options must be set before creating the
+  // ServerContext, otherwise the possible Lua scripts will not be
+  // able to properly issue HTTP/HTTPS queries
+  HttpClient::ConfigureSsl(Configuration::GetGlobalBoolParameter("HttpsVerifyPeers", true),
+                           Configuration::InterpretStringParameterAsPath
+                           (Configuration::GetGlobalStringParameter("HttpsCACertificates", "")));
+  HttpClient::SetDefaultVerbose(Configuration::GetGlobalBoolParameter("HttpVerbose", false));
+  HttpClient::SetDefaultTimeout(Configuration::GetGlobalUnsignedIntegerParameter("HttpTimeout", 0));
+  HttpClient::SetDefaultProxy(Configuration::GetGlobalStringParameter("HttpProxy", ""));
 
-  HttpClient::SetDefaultTimeout(Configuration::GetGlobalIntegerParameter("HttpTimeout", 0));
+  DicomUserConnection::SetDefaultTimeout(Configuration::GetGlobalUnsignedIntegerParameter("DicomScuTimeout", 10));
+
+  ServerContext context(database, storageArea, false /* not running unit tests */);
   context.SetCompressionEnabled(Configuration::GetGlobalBoolParameter("StorageCompression", false));
   context.SetStoreMD5ForAttachments(Configuration::GetGlobalBoolParameter("StoreMD5ForAttachments", true));
 
+  // New option in Orthanc 1.4.2
+  context.GetIndex().SetOverwriteInstances(Configuration::GetGlobalBoolParameter("OverwriteInstances", false));
+
   try
   {
-    context.GetIndex().SetMaximumPatientCount(Configuration::GetGlobalIntegerParameter("MaximumPatientCount", 0));
+    context.GetIndex().SetMaximumPatientCount(Configuration::GetGlobalUnsignedIntegerParameter("MaximumPatientCount", 0));
   }
   catch (...)
   {
@@ -829,7 +1003,7 @@
 
   try
   {
-    uint64_t size = Configuration::GetGlobalIntegerParameter("MaximumStorageSize", 0);
+    uint64_t size = Configuration::GetGlobalUnsignedIntegerParameter("MaximumStorageSize", 0);
     context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
   }
   catch (...)
@@ -837,9 +1011,10 @@
     context.GetIndex().SetMaximumStorageSize(0);
   }
 
-  LoadLuaScripts(context);
+  context.GetJobsEngine().GetRegistry().SetMaxCompletedJobs
+    (Configuration::GetGlobalUnsignedIntegerParameter("JobsHistorySize", 10));
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
   if (plugins)
   {
     plugins->SetServerContext(context);
@@ -847,23 +1022,22 @@
   }
 #endif
 
-  bool restart;
+  bool restart = false;
   ErrorCode error = ErrorCode_Success;
 
   try
   {
-    restart = ConfigureHttpHandler(context, plugins);
+    restart = ConfigureHttpHandler(context, plugins, loadJobsFromDatabase);
   }
   catch (OrthancException& e)
   {
     error = e.GetErrorCode();
   }
 
-  context.Stop();
-
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
   if (plugins)
   {
+    plugins->ResetServerContext();
     context.ResetPlugins();
   }
 #endif
@@ -880,16 +1054,28 @@
 static bool ConfigureDatabase(IDatabaseWrapper& database,
                               IStorageArea& storageArea,
                               OrthancPlugins *plugins,
-                              bool allowDatabaseUpgrade)
+                              bool upgradeDatabase,
+                              bool loadJobsFromDatabase)
 {
   database.Open();
-  
-  if (!UpgradeDatabase(database, storageArea, allowDatabaseUpgrade))
+
+  unsigned int currentVersion = database.GetDatabaseVersion();
+
+  if (upgradeDatabase)
   {
-    return false;
+    UpgradeDatabase(database, storageArea);
+    return false;  // Stop and don't restart Orthanc (cf. issue 29)
+  }
+  else if (currentVersion != ORTHANC_DATABASE_VERSION)
+  {
+    LOG(ERROR) << "The database schema must be changed from version "
+               << currentVersion << " to " << ORTHANC_DATABASE_VERSION 
+               << ": Please run Orthanc with the \"--upgrade\" argument";
+    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
   }
 
-  bool success = ConfigureServerContext(database, storageArea, plugins);
+  bool success = ConfigureServerContext
+    (database, storageArea, plugins, loadJobsFromDatabase);
 
   database.Close();
 
@@ -899,12 +1085,13 @@
 
 static bool ConfigurePlugins(int argc, 
                              char* argv[],
-                             bool allowDatabaseUpgrade)
+                             bool upgradeDatabase,
+                             bool loadJobsFromDatabase)
 {
   std::auto_ptr<IDatabaseWrapper>  databasePtr;
   std::auto_ptr<IStorageArea>  storage;
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
   OrthancPlugins plugins;
   plugins.SetCommandLineArguments(argc, argv);
   LoadPlugins(plugins);
@@ -934,26 +1121,37 @@
   assert(database != NULL);
   assert(storage.get() != NULL);
 
-  return ConfigureDatabase(*database, *storage, &plugins, allowDatabaseUpgrade);
+  return ConfigureDatabase(*database, *storage, &plugins,
+                           upgradeDatabase, loadJobsFromDatabase);
 
-#elif ORTHANC_PLUGINS_ENABLED == 0
+#elif ORTHANC_ENABLE_PLUGINS == 0
   // The plugins are disabled
   databasePtr.reset(Configuration::CreateDatabaseWrapper());
   storage.reset(Configuration::CreateStorageArea());
 
-  return ConfigureDatabase(*databasePtr, *storage, NULL, allowDatabaseUpgrade);
+  return ConfigureDatabase(*databasePtr, *storage, NULL,
+                           upgradeDatabase, loadJobsFromDatabase);
 
 #else
-#  error The macro ORTHANC_PLUGINS_ENABLED must be set to 0 or 1
+#  error The macro ORTHANC_ENABLE_PLUGINS must be set to 0 or 1
 #endif
 }
 
 
 static bool StartOrthanc(int argc, 
                          char* argv[],
-                         bool allowDatabaseUpgrade)
+                         bool upgradeDatabase,
+                         bool loadJobsFromDatabase)
 {
-  return ConfigurePlugins(argc, argv, allowDatabaseUpgrade);
+  return ConfigurePlugins(argc, argv, upgradeDatabase, loadJobsFromDatabase);
+}
+
+
+static bool DisplayPerformanceWarning()
+{
+  (void) DisplayPerformanceWarning;   // Disable warning about unused function
+  LOG(WARNING) << "Performance warning: Non-release build, runtime debug assertions are turned on";
+  return true;
 }
 
 
@@ -961,7 +1159,8 @@
 {
   Logging::Initialize();
 
-  bool allowDatabaseUpgrade = false;
+  bool upgradeDatabase = false;
+  bool loadJobsFromDatabase = true;
   const char* configurationFile = NULL;
 
 
@@ -988,6 +1187,8 @@
       {
         // Use the first argument that does not start with a "-" as
         // the configuration file
+
+        // TODO WHAT IS THE ENCODING?
         configurationFile = argv[i];
       }
     }
@@ -1016,6 +1217,7 @@
     }
     else if (boost::starts_with(argument, "--logdir="))
     {
+      // TODO WHAT IS THE ENCODING?
       std::string directory = argument.substr(9);
 
       try
@@ -1029,12 +1231,33 @@
         return -1;
       }
     }
+    else if (boost::starts_with(argument, "--logfile="))
+    {
+      // TODO WHAT IS THE ENCODING?
+      std::string file = argument.substr(10);
+
+      try
+      {
+        Logging::SetTargetFile(file);
+      }
+      catch (OrthancException&)
+      {
+        LOG(ERROR) << "Cannot write to the specified log file (" 
+                   << file << "), aborting.";
+        return -1;
+      }
+    }
     else if (argument == "--upgrade")
     {
-      allowDatabaseUpgrade = true;
+      upgradeDatabase = true;
+    }
+    else if (argument == "--no-jobs")
+    {
+      loadJobsFromDatabase = false;
     }
     else if (boost::starts_with(argument, "--config="))
     {
+      // TODO WHAT IS THE ENCODING?
       std::string configurationSample;
       GetFileResource(configurationSample, EmbeddedResources::CONFIGURATION_SAMPLE);
 
@@ -1044,8 +1267,17 @@
 #endif
 
       std::string target = argument.substr(9);
-      Toolbox::WriteFile(configurationSample, target);
-      return 0;
+
+      try
+      {
+        SystemToolbox::WriteFile(configurationSample, target);
+        return 0;
+      }
+      catch (OrthancException&)
+      {
+        LOG(ERROR) << "Cannot write sample configuration as file \"" << target << "\"";
+        return -1;
+      }
     }
     else
     {
@@ -1058,7 +1290,26 @@
    * Launch Orthanc.
    **/
 
-  LOG(WARNING) << "Orthanc version: " << ORTHANC_VERSION;
+  {
+    std::string version(ORTHANC_VERSION);
+
+    if (std::string(ORTHANC_VERSION) == "mainline")
+    {
+      try
+      {
+        boost::filesystem::path exe(SystemToolbox::GetPathToExecutable());
+        std::time_t creation = boost::filesystem::last_write_time(exe);
+        boost::posix_time::ptime converted(boost::posix_time::from_time_t(creation));
+        version += " (" + boost::posix_time::to_iso_string(converted) + ")";
+      }
+      catch (...)
+      {
+      }
+    }
+
+    LOG(WARNING) << "Orthanc version: " << version;
+    assert(DisplayPerformanceWarning());
+  }
 
   int status = 0;
   try
@@ -1067,10 +1318,12 @@
     {
       OrthancInitialize(configurationFile);
 
-      bool restart = StartOrthanc(argc, argv, allowDatabaseUpgrade);
+      bool restart = StartOrthanc(argc, argv, upgradeDatabase, loadJobsFromDatabase);
       if (restart)
       {
         OrthancFinalize();
+        LOG(WARNING) << "Logging system is resetting";
+        Logging::Reset();
       }
       else
       {
--- a/Plugins/Engine/IPluginServiceProvider.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Engine/IPluginServiceProvider.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,11 +33,11 @@
 
 #pragma once
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
 
 #include "../Include/orthanc/OrthancCPlugin.h"
 
-#include "SharedLibrary.h"
+#include "../../Core/SharedLibrary.h"
 
 namespace Orthanc
 {
--- a/Plugins/Engine/OrthancPluginDatabase.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Engine/OrthancPluginDatabase.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,7 +34,7 @@
 #include "../../OrthancServer/PrecompiledHeadersServer.h"
 #include "OrthancPluginDatabase.h"
 
-#if ORTHANC_PLUGINS_ENABLED != 1
+#if ORTHANC_ENABLE_PLUGINS != 1
 #error The plugin support is disabled
 #endif
 
@@ -633,6 +634,34 @@
   }
 
 
+  void OrthancPluginDatabase::LookupIdentifierRange(std::list<int64_t>& result,
+                                                    ResourceType level,
+                                                    const DicomTag& tag,
+                                                    const std::string& start,
+                                                    const std::string& end)
+  {
+    if (extensions_.lookupIdentifierRange == NULL)
+    {
+      // Default implementation, for plugins using Orthanc SDK <= 1.3.2
+
+      LookupIdentifier(result, level, tag, IdentifierConstraintType_GreaterOrEqual, start);
+
+      std::list<int64_t> b;
+      LookupIdentifier(result, level, tag, IdentifierConstraintType_SmallerOrEqual, end);
+
+      result.splice(result.end(), b);
+    }
+    else
+    {
+      ResetAnswers();
+      CheckSuccess(extensions_.lookupIdentifierRange(GetContext(), payload_, Plugins::Convert(level),
+                                                     tag.GetGroup(), tag.GetElement(),
+                                                     start.c_str(), end.c_str()));
+      ForwardAnswers(result);
+    }
+  }
+
+
   bool OrthancPluginDatabase::LookupMetadata(std::string& target,
                                              int64_t id,
                                              MetadataType type)
@@ -986,7 +1015,7 @@
       {
         const OrthancPluginDicomTag& tag = *reinterpret_cast<const OrthancPluginDicomTag*>(answer.valueGeneric);
         assert(answerDicomMap_ != NULL);
-        answerDicomMap_->SetValue(tag.group, tag.element, std::string(tag.value));
+        answerDicomMap_->SetValue(tag.group, tag.element, std::string(tag.value), false);
         break;
       }
 
--- a/Plugins/Engine/OrthancPluginDatabase.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Engine/OrthancPluginDatabase.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,12 +33,12 @@
 
 #pragma once
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
 
+#include "../../Core/SharedLibrary.h"
 #include "../../OrthancServer/IDatabaseWrapper.h"
 #include "../Include/orthanc/OrthancCDatabasePlugin.h"
 #include "PluginsErrorDictionary.h"
-#include "SharedLibrary.h"
 
 namespace Orthanc
 {
@@ -212,6 +213,12 @@
                                   IdentifierConstraintType type,
                                   const std::string& value);
 
+    virtual void LookupIdentifierRange(std::list<int64_t>& result,
+                                       ResourceType level,
+                                       const DicomTag& tag,
+                                       const std::string& start,
+                                       const std::string& end);
+
     virtual bool LookupMetadata(std::string& target,
                                 int64_t id,
                                 MetadataType type);
--- a/Plugins/Engine/OrthancPlugins.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Engine/OrthancPlugins.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,20 +34,25 @@
 #include "../../OrthancServer/PrecompiledHeadersServer.h"
 #include "OrthancPlugins.h"
 
-#if ORTHANC_PLUGINS_ENABLED != 1
+#if ORTHANC_ENABLE_PLUGINS != 1
 #error The plugin support is disabled
 #endif
 
 
 #include "../../Core/ChunkedBuffer.h"
+#include "../../Core/DicomFormat/DicomArray.h"
 #include "../../Core/HttpServer/HttpToolbox.h"
 #include "../../Core/Logging.h"
 #include "../../Core/OrthancException.h"
+#include "../../Core/SerializationToolbox.h"
 #include "../../Core/Toolbox.h"
-#include "../../OrthancServer/FromDcmtkBridge.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/DicomParsing/ToDcmtkBridge.h"
 #include "../../OrthancServer/OrthancInitialization.h"
 #include "../../OrthancServer/ServerContext.h"
 #include "../../OrthancServer/ServerToolbox.h"
+#include "../../OrthancServer/Search/HierarchicalMatcher.h"
+#include "../../Core/DicomParsing/Internals/DicomImageDecoder.h"
 #include "../../Core/Compression/ZlibCompressor.h"
 #include "../../Core/Compression/GzipCompressor.h"
 #include "../../Core/Images/Image.h"
@@ -55,12 +61,78 @@
 #include "../../Core/Images/JpegReader.h"
 #include "../../Core/Images/JpegWriter.h"
 #include "../../Core/Images/ImageProcessing.h"
+#include "../../OrthancServer/DefaultDicomImageDecoder.h"
+#include "../../OrthancServer/OrthancFindRequestHandler.h"
 #include "PluginsEnumerations.h"
+#include "PluginsJob.h"
 
 #include <boost/regex.hpp> 
+#include <dcmtk/dcmdata/dcdict.h>
+#include <dcmtk/dcmdata/dcdicent.h>
 
 namespace Orthanc
 {
+  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
+                                 const void* data,
+                                 size_t size)
+  {
+    target.size = size;
+
+    if (size == 0)
+    {
+      target.data = NULL;
+    }
+    else
+    {
+      target.data = malloc(size);
+      if (target.data != NULL)
+      {
+        memcpy(target.data, data, size);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+    }
+  }
+
+
+  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
+                                 const std::string& str)
+  {
+    if (str.size() == 0)
+    {
+      target.size = 0;
+      target.data = NULL;
+    }
+    else
+    {
+      CopyToMemoryBuffer(target, str.c_str(), str.size());
+    }
+  }
+
+
+  static char* CopyString(const std::string& str)
+  {
+    char *result = reinterpret_cast<char*>(malloc(str.size() + 1));
+    if (result == NULL)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+
+    if (str.size() == 0)
+    {
+      result[0] = '\0';
+    }
+    else
+    {
+      memcpy(result, &str[0], str.size() + 1);
+    }
+
+    return result;
+  }
+
+
   namespace
   {
     class PluginStorageArea : public IStorageArea
@@ -179,11 +251,72 @@
         return new PluginStorageArea(callbacks_, errorDictionary_);
       }
     };
+
+
+    class OrthancPeers : public boost::noncopyable
+    {
+    private:
+      std::vector<std::string>           names_;
+      std::vector<WebServiceParameters>  parameters_;
+
+      void CheckIndex(size_t i) const
+      {
+        assert(names_.size() == parameters_.size());
+        if (i >= names_.size())
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+      }
+      
+    public:
+      OrthancPeers()
+      {
+        std::set<std::string> peers;
+        Configuration::GetListOfOrthancPeers(peers);
+
+        names_.reserve(peers.size());
+        parameters_.reserve(peers.size());
+        
+        for (std::set<std::string>::const_iterator
+               it = peers.begin(); it != peers.end(); ++it)
+        {
+          WebServiceParameters peer;
+          if (Configuration::GetOrthancPeer(peer, *it))
+          {
+            names_.push_back(*it);
+            parameters_.push_back(peer);
+          }
+        }
+      }
+
+      size_t GetPeersCount() const
+      {
+        return names_.size();
+      }
+
+      const std::string& GetPeerName(size_t i) const
+      {
+        CheckIndex(i);
+        return names_[i];
+      }
+
+      const WebServiceParameters& GetPeerParameters(size_t i) const
+      {
+        CheckIndex(i);
+        return parameters_[i];
+      }
+    };
   }
 
 
-  struct OrthancPlugins::PImpl
+  class OrthancPlugins::PImpl
   {
+  private:
+    boost::mutex   contextMutex_;
+    ServerContext* context_;
+    
+
+  public:
     class RestCallback : public boost::noncopyable
     {
     private:
@@ -233,22 +366,71 @@
     };
 
 
+    class ServerContextLock
+    {
+    private:
+      boost::mutex::scoped_lock  lock_;
+      ServerContext* context_;
+
+    public:
+      ServerContextLock(PImpl& that) : 
+        lock_(that.contextMutex_),
+        context_(that.context_)
+      {
+        if (context_ == NULL)
+        {
+          throw OrthancException(ErrorCode_DatabaseNotInitialized);
+        }
+      }
+
+      ServerContext& GetContext()
+      {
+        assert(context_ != NULL);
+        return *context_;
+      }
+    };
+
+
+    void SetServerContext(ServerContext* context)
+    {
+      boost::mutex::scoped_lock lock(contextMutex_);
+      context_ = context;
+    }
+
+
     typedef std::pair<std::string, _OrthancPluginProperty>  Property;
     typedef std::list<RestCallback*>  RestCallbacks;
     typedef std::list<OrthancPluginOnStoredInstanceCallback>  OnStoredCallbacks;
     typedef std::list<OrthancPluginOnChangeCallback>  OnChangeCallbacks;
+    typedef std::list<OrthancPluginIncomingHttpRequestFilter>  IncomingHttpRequestFilters;
+    typedef std::list<OrthancPluginIncomingHttpRequestFilter2>  IncomingHttpRequestFilters2;
+    typedef std::list<OrthancPluginDecodeImageCallback>  DecodeImageCallbacks;
+    typedef std::list<OrthancPluginJobsUnserializer>  JobsUnserializers;
     typedef std::map<Property, std::string>  Properties;
 
     PluginsManager manager_;
-    ServerContext* context_;
+
     RestCallbacks restCallbacks_;
     OnStoredCallbacks  onStoredCallbacks_;
     OnChangeCallbacks  onChangeCallbacks_;
+    OrthancPluginFindCallback  findCallback_;
+    OrthancPluginWorklistCallback  worklistCallback_;
+    DecodeImageCallbacks  decodeImageCallbacks_;
+    JobsUnserializers  jobsUnserializers_;
+    _OrthancPluginMoveCallback moveCallbacks_;
+    IncomingHttpRequestFilters  incomingHttpRequestFilters_;
+    IncomingHttpRequestFilters2 incomingHttpRequestFilters2_;
     std::auto_ptr<StorageAreaFactory>  storageArea_;
+
     boost::recursive_mutex restCallbackMutex_;
     boost::recursive_mutex storedCallbackMutex_;
     boost::recursive_mutex changeCallbackMutex_;
+    boost::mutex findCallbackMutex_;
+    boost::mutex worklistCallbackMutex_;
+    boost::mutex decodeImageCallbackMutex_;
+    boost::mutex jobsUnserializersMutex_;
     boost::recursive_mutex invokeServiceMutex_;
+
     Properties properties_;
     int argc_;
     char** argv_;
@@ -257,37 +439,396 @@
 
     PImpl() : 
       context_(NULL), 
+      findCallback_(NULL),
+      worklistCallback_(NULL),
       argc_(1),
       argv_(NULL)
     {
+      memset(&moveCallbacks_, 0, sizeof(moveCallbacks_));
     }
   };
 
 
   
-  static char* CopyString(const std::string& str)
+  class OrthancPlugins::WorklistHandler : public IWorklistRequestHandler
   {
-    char *result = reinterpret_cast<char*>(malloc(str.size() + 1));
-    if (result == NULL)
+  private:
+    OrthancPlugins&  that_;
+    std::auto_ptr<HierarchicalMatcher> matcher_;
+    std::auto_ptr<ParsedDicomFile>     filtered_;
+    ParsedDicomFile* currentQuery_;
+
+    void Reset()
+    {
+      matcher_.reset();
+      filtered_.reset();
+      currentQuery_ = NULL;
+    }
+
+  public:
+    WorklistHandler(OrthancPlugins& that) : that_(that)
+    {
+      Reset();
+    }
+
+    virtual void Handle(DicomFindAnswers& answers,
+                        ParsedDicomFile& query,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet,
+                        ModalityManufacturer manufacturer)
+    {
+      static const char* LUA_CALLBACK = "IncomingWorklistRequestFilter";
+
+      {
+        PImpl::ServerContextLock lock(*that_.pimpl_);
+        LuaScripting::Lock lua(lock.GetContext().GetLuaScripting());
+
+        if (!lua.GetLua().IsExistingFunction(LUA_CALLBACK))
+        {
+          currentQuery_ = &query;
+        }
+        else
+        {
+          Json::Value source, origin;
+          query.DatasetToJson(source, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
+
+          OrthancFindRequestHandler::FormatOrigin
+            (origin, remoteIp, remoteAet, calledAet, manufacturer);
+
+          LuaFunctionCall call(lua.GetLua(), LUA_CALLBACK);
+          call.PushJson(source);
+          call.PushJson(origin);
+
+          Json::Value target;
+          call.ExecuteToJson(target, true);
+          
+          filtered_.reset(ParsedDicomFile::CreateFromJson(target, DicomFromJsonFlags_None));
+          currentQuery_ = filtered_.get();
+        }
+      }
+      
+      matcher_.reset(new HierarchicalMatcher(*currentQuery_));
+
+      {
+        boost::mutex::scoped_lock lock(that_.pimpl_->worklistCallbackMutex_);
+
+        if (that_.pimpl_->worklistCallback_)
+        {
+          OrthancPluginErrorCode error = that_.pimpl_->worklistCallback_
+            (reinterpret_cast<OrthancPluginWorklistAnswers*>(&answers),
+             reinterpret_cast<const OrthancPluginWorklistQuery*>(this),
+             remoteAet.c_str(),
+             calledAet.c_str());
+
+          if (error != OrthancPluginErrorCode_Success)
+          {
+            Reset();
+            that_.GetErrorDictionary().LogError(error, true);
+            throw OrthancException(static_cast<ErrorCode>(error));
+          }
+        }
+
+        Reset();
+      }
+    }
+
+    void GetDicomQuery(OrthancPluginMemoryBuffer& target) const
     {
-      throw OrthancException(ErrorCode_NotEnoughMemory);
+      if (currentQuery_ == NULL)
+      {
+        throw OrthancException(ErrorCode_Plugin);
+      }
+
+      std::string dicom;
+      currentQuery_->SaveToMemoryBuffer(dicom);
+      CopyToMemoryBuffer(target, dicom.c_str(), dicom.size());
+    }
+
+    bool IsMatch(const void* dicom,
+                 size_t size) const
+    {
+      if (matcher_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_Plugin);
+      }
+
+      ParsedDicomFile f(dicom, size);
+      return matcher_->Match(f);
+    }
+
+    void AddAnswer(OrthancPluginWorklistAnswers* answers,
+                   const void* dicom,
+                   size_t size) const
+    {
+      if (matcher_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_Plugin);
+      }
+
+      ParsedDicomFile f(dicom, size);
+      std::auto_ptr<ParsedDicomFile> summary(matcher_->Extract(f));
+      reinterpret_cast<DicomFindAnswers*>(answers)->Add(*summary);
+    }
+  };
+
+  
+  class OrthancPlugins::FindHandler : public IFindRequestHandler
+  {
+  private:
+    OrthancPlugins&            that_;
+    std::auto_ptr<DicomArray>  currentQuery_;
+
+    void Reset()
+    {
+      currentQuery_.reset(NULL);
     }
 
-    if (str.size() == 0)
+  public:
+    FindHandler(OrthancPlugins& that) : that_(that)
+    {
+      Reset();
+    }
+
+    virtual void Handle(DicomFindAnswers& answers,
+                        const DicomMap& input,
+                        const std::list<DicomTag>& sequencesToReturn,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet,
+                        ModalityManufacturer manufacturer)
+    {
+      DicomMap tmp;
+      tmp.Assign(input);
+
+      for (std::list<DicomTag>::const_iterator it = sequencesToReturn.begin(); 
+           it != sequencesToReturn.end(); ++it)
+      {
+        if (!input.HasTag(*it))
+        {
+          tmp.SetValue(*it, "", false);
+        }
+      }      
+
+      {
+        boost::mutex::scoped_lock lock(that_.pimpl_->findCallbackMutex_);
+        currentQuery_.reset(new DicomArray(tmp));
+
+        if (that_.pimpl_->findCallback_)
+        {
+          OrthancPluginErrorCode error = that_.pimpl_->findCallback_
+            (reinterpret_cast<OrthancPluginFindAnswers*>(&answers),
+             reinterpret_cast<const OrthancPluginFindQuery*>(this),
+             remoteAet.c_str(),
+             calledAet.c_str());
+
+          if (error != OrthancPluginErrorCode_Success)
+          {
+            Reset();
+            that_.GetErrorDictionary().LogError(error, true);
+            throw OrthancException(static_cast<ErrorCode>(error));
+          }
+        }
+
+        Reset();
+      }
+    }
+
+    void Invoke(_OrthancPluginService service,
+                const _OrthancPluginFindOperation& operation) const
     {
-      result[0] = '\0';
+      if (currentQuery_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_Plugin);
+      }
+
+      switch (service)
+      {
+        case _OrthancPluginService_GetFindQuerySize:
+          *operation.resultUint32 = currentQuery_->GetSize();
+          break;
+
+        case _OrthancPluginService_GetFindQueryTag:
+        {
+          const DicomTag& tag = currentQuery_->GetElement(operation.index).GetTag();
+          *operation.resultGroup = tag.GetGroup();
+          *operation.resultElement = tag.GetElement();
+          break;
+        }
+
+        case _OrthancPluginService_GetFindQueryTagName:
+        {
+          const DicomElement& element = currentQuery_->GetElement(operation.index);
+          *operation.resultString = CopyString(FromDcmtkBridge::GetTagName(element));
+          break;
+        }
+
+        case _OrthancPluginService_GetFindQueryValue:
+        {
+          *operation.resultString = CopyString(currentQuery_->GetElement(operation.index).GetValue().GetContent());
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
     }
-    else
+  };
+  
+
+
+  class OrthancPlugins::MoveHandler : public IMoveRequestHandler
+  {
+  private:
+    class Driver : public IMoveRequestIterator
     {
-      memcpy(result, &str[0], str.size() + 1);
+    private:
+      void*                   driver_;
+      unsigned int            count_;
+      unsigned int            pos_;
+      OrthancPluginApplyMove  apply_;
+      OrthancPluginFreeMove   free_;
+
+    public:
+      Driver(void* driver,
+             unsigned int count,
+             OrthancPluginApplyMove apply,
+             OrthancPluginFreeMove free) :
+        driver_(driver),
+        count_(count),
+        pos_(0),
+        apply_(apply),
+        free_(free)
+      {
+        if (driver_ == NULL)
+        {
+          throw OrthancException(ErrorCode_Plugin);
+        }
+      }
+
+      virtual ~Driver()
+      {
+        if (driver_ != NULL)
+        {
+          free_(driver_);
+          driver_ = NULL;
+        }
+      }
+
+      virtual unsigned int GetSubOperationCount() const
+      {
+        return count_;
+      }
+
+      virtual Status DoNext()
+      {
+        if (pos_ >= count_)
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+        else
+        {
+          OrthancPluginErrorCode error = apply_(driver_);
+          if (error != OrthancPluginErrorCode_Success)
+          {
+            LOG(ERROR) << "Error while doing C-Move from plugin: " << EnumerationToString(static_cast<ErrorCode>(error));
+            return Status_Failure;
+          }
+          else
+          {
+            pos_++;
+            return Status_Success;
+          }
+        }
+      }
+    };
+
+
+    _OrthancPluginMoveCallback  params_;
+
+
+    static std::string ReadTag(const DicomMap& input,
+                               const DicomTag& tag)
+    {
+      const DicomValue* value = input.TestAndGetValue(tag);
+      if (value != NULL &&
+          !value->IsBinary() &&
+          !value->IsNull())
+      {
+        return value->GetContent();
+      }
+      else
+      {
+        return std::string();
+      }
     }
-
-    return result;
-  }
+                        
+
+
+  public:
+    MoveHandler(OrthancPlugins& that)
+    {
+      boost::recursive_mutex::scoped_lock lock(that.pimpl_->invokeServiceMutex_);
+      params_ = that.pimpl_->moveCallbacks_;
+      
+      if (params_.callback == NULL ||
+          params_.getMoveSize == NULL ||
+          params_.applyMove == NULL ||
+          params_.freeMove == NULL)
+      {
+        throw OrthancException(ErrorCode_Plugin);
+      }
+    }
+
+    virtual IMoveRequestIterator* Handle(const std::string& targetAet,
+                                         const DicomMap& input,
+                                         const std::string& originatorIp,
+                                         const std::string& originatorAet,
+                                         const std::string& calledAet,
+                                         uint16_t originatorId)
+    {
+      std::string levelString = ReadTag(input, DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+      std::string patientId = ReadTag(input, DICOM_TAG_PATIENT_ID);
+      std::string accessionNumber = ReadTag(input, DICOM_TAG_ACCESSION_NUMBER);
+      std::string studyInstanceUid = ReadTag(input, DICOM_TAG_STUDY_INSTANCE_UID);
+      std::string seriesInstanceUid = ReadTag(input, DICOM_TAG_SERIES_INSTANCE_UID);
+      std::string sopInstanceUid = ReadTag(input, DICOM_TAG_SOP_INSTANCE_UID);
+
+      OrthancPluginResourceType level = OrthancPluginResourceType_None;
+
+      if (!levelString.empty())
+      {
+        level = Plugins::Convert(StringToResourceType(levelString.c_str()));
+      }
+
+      void* driver = params_.callback(level,
+                                      patientId.empty() ? NULL : patientId.c_str(),
+                                      accessionNumber.empty() ? NULL : accessionNumber.c_str(),
+                                      studyInstanceUid.empty() ? NULL : studyInstanceUid.c_str(),
+                                      seriesInstanceUid.empty() ? NULL : seriesInstanceUid.c_str(),
+                                      sopInstanceUid.empty() ? NULL : sopInstanceUid.c_str(),
+                                      originatorAet.c_str(),
+                                      calledAet.c_str(),
+                                      targetAet.c_str(),
+                                      originatorId);
+
+      if (driver == NULL)
+      {
+        LOG(ERROR) << "Plugin cannot create a driver for an incoming C-MOVE request";
+        throw OrthancException(ErrorCode_Plugin);
+      }
+
+      unsigned int size = params_.getMoveSize(driver);
+
+      return new Driver(driver, size, params_.applyMove, params_.freeMove);
+    }
+  };
+  
 
 
   OrthancPlugins::OrthancPlugins()
   {
+    /* Sanity check of the compiler */
     if (sizeof(int32_t) != sizeof(OrthancPluginErrorCode) ||
         sizeof(int32_t) != sizeof(OrthancPluginHttpMethod) ||
         sizeof(int32_t) != sizeof(_OrthancPluginService) ||
@@ -301,16 +842,21 @@
         sizeof(int32_t) != sizeof(OrthancPluginValueRepresentation) ||
         sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) ||
         sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) ||
+        sizeof(int32_t) != sizeof(OrthancPluginCreateDicomFlags) ||
         sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) ||
         sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) ||
+        sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) ||
+        sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeBinary) != static_cast<int>(DicomToJsonFlags_IncludeBinary) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludePrivateTags) != static_cast<int>(DicomToJsonFlags_IncludePrivateTags) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeUnknownTags) != static_cast<int>(DicomToJsonFlags_IncludeUnknownTags) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludePixelData) != static_cast<int>(DicomToJsonFlags_IncludePixelData) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_ConvertBinaryToNull) != static_cast<int>(DicomToJsonFlags_ConvertBinaryToNull) ||
-        static_cast<int>(OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii) != static_cast<int>(DicomToJsonFlags_ConvertBinaryToAscii))
+        static_cast<int>(OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii) != static_cast<int>(DicomToJsonFlags_ConvertBinaryToAscii) ||
+        static_cast<int>(OrthancPluginCreateDicomFlags_DecodeDataUriScheme) != static_cast<int>(DicomFromJsonFlags_DecodeDataUriScheme) ||
+        static_cast<int>(OrthancPluginCreateDicomFlags_GenerateIdentifiers) != static_cast<int>(DicomFromJsonFlags_GenerateIdentifiers))
+
     {
-      /* Sanity check of the compiler */
       throw OrthancException(ErrorCode_Plugin);
     }
 
@@ -321,10 +867,15 @@
   
   void OrthancPlugins::SetServerContext(ServerContext& context)
   {
-    pimpl_->context_ = &context;
+    pimpl_->SetServerContext(&context);
   }
 
 
+  void OrthancPlugins::ResetServerContext()
+  {
+    pimpl_->SetServerContext(NULL);
+  }
+
   
   OrthancPlugins::~OrthancPlugins()
   {
@@ -485,7 +1036,7 @@
     }
     else
     {
-      GetErrorDictionary().LogError(error, true);
+      GetErrorDictionary().LogError(error, false);
       throw OrthancException(static_cast<ErrorCode>(error));
     }
   }
@@ -546,46 +1097,6 @@
 
 
 
-  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
-                                 const void* data,
-                                 size_t size)
-  {
-    target.size = size;
-
-    if (size == 0)
-    {
-      target.data = NULL;
-    }
-    else
-    {
-      target.data = malloc(size);
-      if (target.data != NULL)
-      {
-        memcpy(target.data, data, size);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_NotEnoughMemory);
-      }
-    }
-  }
-
-
-  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
-                                 const std::string& str)
-  {
-    if (str.size() == 0)
-    {
-      target.size = 0;
-      target.data = NULL;
-    }
-    else
-    {
-      CopyToMemoryBuffer(target, str.c_str(), str.size());
-    }
-  }
-
-
   void OrthancPlugins::RegisterRestCallback(const void* parameters,
                                             bool lock)
   {
@@ -593,7 +1104,7 @@
       *reinterpret_cast<const _OrthancPluginRestCallback*>(parameters);
 
     LOG(INFO) << "Plugin has registered a REST callback "
-              << (lock ? "with" : "witout")
+              << (lock ? "with" : "without")
               << " mutual exclusion on: " 
               << p.pathRegularExpression;
 
@@ -622,6 +1133,111 @@
   }
 
 
+  void OrthancPlugins::RegisterWorklistCallback(const void* parameters)
+  {
+    const _OrthancPluginWorklistCallback& p = 
+      *reinterpret_cast<const _OrthancPluginWorklistCallback*>(parameters);
+
+    boost::mutex::scoped_lock lock(pimpl_->worklistCallbackMutex_);
+
+    if (pimpl_->worklistCallback_ != NULL)
+    {
+      LOG(ERROR) << "Can only register one plugin to handle modality worklists";
+      throw OrthancException(ErrorCode_Plugin);
+    }
+    else
+    {
+      LOG(INFO) << "Plugin has registered a callback to handle modality worklists";
+      pimpl_->worklistCallback_ = p.callback;
+    }
+  }
+
+
+  void OrthancPlugins::RegisterFindCallback(const void* parameters)
+  {
+    const _OrthancPluginFindCallback& p = 
+      *reinterpret_cast<const _OrthancPluginFindCallback*>(parameters);
+
+    boost::mutex::scoped_lock lock(pimpl_->findCallbackMutex_);
+
+    if (pimpl_->findCallback_ != NULL)
+    {
+      LOG(ERROR) << "Can only register one plugin to handle C-FIND requests";
+      throw OrthancException(ErrorCode_Plugin);
+    }
+    else
+    {
+      LOG(INFO) << "Plugin has registered a callback to handle C-FIND requests";
+      pimpl_->findCallback_ = p.callback;
+    }
+  }
+
+
+  void OrthancPlugins::RegisterMoveCallback(const void* parameters)
+  {
+    // invokeServiceMutex_ is assumed to be locked
+
+    const _OrthancPluginMoveCallback& p = 
+      *reinterpret_cast<const _OrthancPluginMoveCallback*>(parameters);
+
+    if (pimpl_->moveCallbacks_.callback != NULL)
+    {
+      LOG(ERROR) << "Can only register one plugin to handle C-MOVE requests";
+      throw OrthancException(ErrorCode_Plugin);
+    }
+    else
+    {
+      LOG(INFO) << "Plugin has registered a callback to handle C-MOVE requests";
+      pimpl_->moveCallbacks_ = p;
+    }
+  }
+
+
+  void OrthancPlugins::RegisterDecodeImageCallback(const void* parameters)
+  {
+    const _OrthancPluginDecodeImageCallback& p = 
+      *reinterpret_cast<const _OrthancPluginDecodeImageCallback*>(parameters);
+
+    boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_);
+
+    pimpl_->decodeImageCallbacks_.push_back(p.callback);
+    LOG(INFO) << "Plugin has registered a callback to decode DICOM images (" 
+              << pimpl_->decodeImageCallbacks_.size() << " decoder(s) now active)";
+  }
+
+
+  void OrthancPlugins::RegisterJobsUnserializer(const void* parameters)
+  {
+    const _OrthancPluginJobsUnserializer& p = 
+      *reinterpret_cast<const _OrthancPluginJobsUnserializer*>(parameters);
+
+    boost::mutex::scoped_lock lock(pimpl_->jobsUnserializersMutex_);
+
+    pimpl_->jobsUnserializers_.push_back(p.unserializer);
+    LOG(INFO) << "Plugin has registered a callback to unserialize jobs (" 
+              << pimpl_->jobsUnserializers_.size() << " unserializer(s) now active)";
+  }
+
+
+  void OrthancPlugins::RegisterIncomingHttpRequestFilter(const void* parameters)
+  {
+    const _OrthancPluginIncomingHttpRequestFilter& p = 
+      *reinterpret_cast<const _OrthancPluginIncomingHttpRequestFilter*>(parameters);
+
+    LOG(INFO) << "Plugin has registered a callback to filter incoming HTTP requests";
+    pimpl_->incomingHttpRequestFilters_.push_back(p.callback);
+  }
+
+
+  void OrthancPlugins::RegisterIncomingHttpRequestFilter2(const void* parameters)
+  {
+    const _OrthancPluginIncomingHttpRequestFilter2& p = 
+      *reinterpret_cast<const _OrthancPluginIncomingHttpRequestFilter2*>(parameters);
+
+    LOG(INFO) << "Plugin has registered a callback to filter incoming HTTP requests";
+    pimpl_->incomingHttpRequestFilters2_.push_back(p.callback);
+  }
+
 
   void OrthancPlugins::AnswerBuffer(const void* parameters)
   {
@@ -772,15 +1388,6 @@
   }
 
 
-  void OrthancPlugins::CheckContextAvailable()
-  {
-    if (!pimpl_->context_)
-    {
-      throw OrthancException(ErrorCode_DatabaseNotInitialized);
-    }
-  }
-
-
   void OrthancPlugins::GetDicomForInstance(const void* parameters)
   {
     const _OrthancPluginGetDicomForInstance& p = 
@@ -788,8 +1395,10 @@
 
     std::string dicom;
 
-    CheckContextAvailable();
-    pimpl_->context_->ReadFile(dicom, p.instanceId, FileContentType_Dicom);
+    {
+      PImpl::ServerContextLock lock(*pimpl_);
+      lock.GetContext().ReadDicom(dicom, p.instanceId);
+    }
 
     CopyToMemoryBuffer(*p.target, dicom);
   }
@@ -804,17 +1413,57 @@
     LOG(INFO) << "Plugin making REST GET call on URI " << p.uri
               << (afterPlugins ? " (after plugins)" : " (built-in API)");
 
-    CheckContextAvailable();
-    IHttpHandler& handler = pimpl_->context_->GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
+    IHttpHandler* handler;
+
+    {
+      PImpl::ServerContextLock lock(*pimpl_);
+      handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
+    }
 
     std::string result;
-    if (HttpToolbox::SimpleGet(result, handler, RequestOrigin_Plugins, p.uri))
+    if (HttpToolbox::SimpleGet(result, *handler, RequestOrigin_Plugins, p.uri))
     {
       CopyToMemoryBuffer(*p.target, result);
     }
     else
     {
-      throw OrthancException(ErrorCode_BadRequest);
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+
+
+  void OrthancPlugins::RestApiGet2(const void* parameters)
+  {
+    const _OrthancPluginRestApiGet2& p = 
+      *reinterpret_cast<const _OrthancPluginRestApiGet2*>(parameters);
+        
+    LOG(INFO) << "Plugin making REST GET call on URI " << p.uri
+              << (p.afterPlugins ? " (after plugins)" : " (built-in API)");
+
+    IHttpHandler::Arguments headers;
+
+    for (uint32_t i = 0; i < p.headersCount; i++)
+    {
+      std::string name(p.headersKeys[i]);
+      std::transform(name.begin(), name.end(), name.begin(), ::tolower);
+      headers[name] = p.headersValues[i];
+    }
+
+    IHttpHandler* handler;
+
+    {
+      PImpl::ServerContextLock lock(*pimpl_);
+      handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!p.afterPlugins);
+    }
+      
+    std::string result;
+    if (HttpToolbox::SimpleGet(result, *handler, RequestOrigin_Plugins, p.uri, headers))
+    {
+      CopyToMemoryBuffer(*p.target, result);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
     }
   }
 
@@ -829,19 +1478,23 @@
     LOG(INFO) << "Plugin making REST " << EnumerationToString(isPost ? HttpMethod_Post : HttpMethod_Put)
               << " call on URI " << p.uri << (afterPlugins ? " (after plugins)" : " (built-in API)");
 
-    CheckContextAvailable();
-    IHttpHandler& handler = pimpl_->context_->GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
-
+    IHttpHandler* handler;
+
+    {
+      PImpl::ServerContextLock lock(*pimpl_);
+      handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
+    }
+      
     std::string result;
     if (isPost ? 
-        HttpToolbox::SimplePost(result, handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize) :
-        HttpToolbox::SimplePut (result, handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize))
+        HttpToolbox::SimplePost(result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize) :
+        HttpToolbox::SimplePut (result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize))
     {
       CopyToMemoryBuffer(*p.target, result);
     }
     else
     {
-      throw OrthancException(ErrorCode_BadRequest);
+      throw OrthancException(ErrorCode_UnknownResource);
     }
   }
 
@@ -853,12 +1506,16 @@
     LOG(INFO) << "Plugin making REST DELETE call on URI " << uri
               << (afterPlugins ? " (after plugins)" : " (built-in API)");
 
-    CheckContextAvailable();
-    IHttpHandler& handler = pimpl_->context_->GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
-
-    if (!HttpToolbox::SimpleDelete(handler, RequestOrigin_Plugins, uri))
+    IHttpHandler* handler;
+
     {
-      throw OrthancException(ErrorCode_BadRequest);
+      PImpl::ServerContextLock lock(*pimpl_);
+      handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
+    }
+      
+    if (!HttpToolbox::SimpleDelete(*handler, RequestOrigin_Plugins, uri))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
     }
   }
 
@@ -909,10 +1566,12 @@
         throw OrthancException(ErrorCode_InternalError);
     }
 
-    CheckContextAvailable();
-
     std::list<std::string> result;
-    pimpl_->context_->GetIndex().LookupIdentifierExact(result, level, tag, p.argument);
+
+    {
+      PImpl::ServerContextLock lock(*pimpl_);
+      lock.GetContext().GetIndex().LookupIdentifierExact(result, level, tag, p.argument);
+    }
 
     if (result.size() == 1)
     {
@@ -991,7 +1650,7 @@
     switch (service)
     {
       case _OrthancPluginService_GetInstanceRemoteAet:
-        *p.resultString = instance.GetRemoteAet();
+        *p.resultString = instance.GetOrigin().GetRemoteAetC();
         return;
 
       case _OrthancPluginService_GetInstanceSize:
@@ -1023,7 +1682,7 @@
         else
         {
           Json::Value simplified;
-          Toolbox::SimplifyTags(simplified, instance.GetJson());
+          ServerToolbox::SimplifyTags(simplified, instance.GetJson(), DicomToJsonFormat_Human);
           s = writer.write(simplified);
         }
 
@@ -1031,6 +1690,10 @@
         return;
       }
 
+      case _OrthancPluginService_GetInstanceOrigin:   // New in Orthanc 0.9.5
+        *p.resultOrigin = Plugins::Convert(instance.GetOrigin().GetRequestOrigin());
+        return;
+
       default:
         throw OrthancException(ErrorCode_InternalError);
     }
@@ -1095,6 +1758,25 @@
   }
 
 
+  static OrthancPluginImage* ReturnImage(std::auto_ptr<ImageAccessor>& image)
+  {
+    // Images returned to plugins are assumed to be writeable. If the
+    // input image is read-only, we return a copy so that it can be modified.
+
+    if (image->IsReadOnly())
+    {
+      std::auto_ptr<Image> copy(new Image(image->GetFormat(), image->GetWidth(), image->GetHeight(), false));
+      ImageProcessing::Copy(*copy, *image);
+      image.reset(NULL);
+      return reinterpret_cast<OrthancPluginImage*>(copy.release());
+    }
+    else
+    {
+      return reinterpret_cast<OrthancPluginImage*>(image.release());
+    }
+  }
+
+
   void OrthancPlugins::UncompressImage(const void* parameters)
   {
     const _OrthancPluginUncompressImage& p = *reinterpret_cast<const _OrthancPluginUncompressImage*>(parameters);
@@ -1117,11 +1799,17 @@
         break;
       }
 
+      case OrthancPluginImageFormat_Dicom:
+      {
+        image.reset(Decode(p.data, p.size, 0));
+        break;
+      }
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-    *(p.target) = reinterpret_cast<OrthancPluginImage*>(image.release());
+    *(p.target) = ReturnImage(image);
   }
 
 
@@ -1131,12 +1819,15 @@
 
     std::string compressed;
 
+    ImageAccessor accessor;
+    accessor.AssignReadOnly(Plugins::Convert(p.pixelFormat), p.width, p.height, p.pitch, p.buffer);
+
     switch (p.imageFormat)
     {
       case OrthancPluginImageFormat_Png:
       {
         PngWriter writer;
-        writer.WriteToMemory(compressed, p.width, p.height, p.pitch, Plugins::Convert(p.pixelFormat), p.buffer);
+        writer.WriteToMemory(compressed, accessor);
         break;
       }
 
@@ -1144,7 +1835,7 @@
       {
         JpegWriter writer;
         writer.SetQuality(p.quality);
-        writer.WriteToMemory(compressed, p.width, p.height, p.pitch, Plugins::Convert(p.pixelFormat), p.buffer);
+        writer.WriteToMemory(compressed, accessor);
         break;
       }
 
@@ -1203,15 +1894,209 @@
   }
 
 
+  void OrthancPlugins::CallHttpClient2(const void* parameters)
+  {
+    const _OrthancPluginCallHttpClient2& p = *reinterpret_cast<const _OrthancPluginCallHttpClient2*>(parameters);
+
+    HttpClient client;
+    client.SetUrl(p.url);
+    client.SetConvertHeadersToLowerCase(false);
+
+    if (p.timeout != 0)
+    {
+      client.SetTimeout(p.timeout);
+    }
+
+    if (p.username != NULL && 
+        p.password != NULL)
+    {
+      client.SetCredentials(p.username, p.password);
+    }
+
+    if (p.certificateFile != NULL)
+    {
+      std::string certificate(p.certificateFile);
+      std::string key, password;
+
+      if (p.certificateKeyFile)
+      {
+        key.assign(p.certificateKeyFile);
+      }
+
+      if (p.certificateKeyPassword)
+      {
+        password.assign(p.certificateKeyPassword);
+      }
+
+      client.SetClientCertificate(certificate, key, password);
+    }
+
+    client.SetPkcs11Enabled(p.pkcs11 ? true : false);
+
+    for (uint32_t i = 0; i < p.headersCount; i++)
+    {
+      if (p.headersKeys[i] == NULL ||
+          p.headersValues[i] == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+
+      client.AddHeader(p.headersKeys[i], p.headersValues[i]);
+    }
+
+    switch (p.method)
+    {
+      case OrthancPluginHttpMethod_Get:
+        client.SetMethod(HttpMethod_Get);
+        break;
+
+      case OrthancPluginHttpMethod_Post:
+        client.SetMethod(HttpMethod_Post);
+        client.GetBody().assign(p.body, p.bodySize);
+        break;
+
+      case OrthancPluginHttpMethod_Put:
+        client.SetMethod(HttpMethod_Put);
+        client.GetBody().assign(p.body, p.bodySize);
+        break;
+
+      case OrthancPluginHttpMethod_Delete:
+        client.SetMethod(HttpMethod_Delete);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    std::string body;
+    HttpClient::HttpHeaders headers;
+
+    bool success = client.Apply(body, headers);
+
+    // The HTTP request has succeeded
+    *p.httpStatus = static_cast<uint16_t>(client.GetLastStatus());
+
+    if (!success)
+    {
+      HttpClient::ThrowException(client.GetLastStatus());
+    }
+
+    // Copy the HTTP headers of the answer, if the plugin requested them
+    if (p.answerHeaders != NULL)
+    {
+      Json::Value json = Json::objectValue;
+
+      for (HttpClient::HttpHeaders::const_iterator 
+             it = headers.begin(); it != headers.end(); ++it)
+      {
+        json[it->first] = it->second;
+      }
+        
+      std::string s = json.toStyledString();
+      CopyToMemoryBuffer(*p.answerHeaders, s);
+    }
+
+    // Copy the body of the answer if it makes sense
+    if (p.method != OrthancPluginHttpMethod_Delete)
+    {
+      CopyToMemoryBuffer(*p.answerBody, body);
+    }
+  }
+
+
+  void OrthancPlugins::CallPeerApi(const void* parameters)
+  {
+    const _OrthancPluginCallPeerApi& p = *reinterpret_cast<const _OrthancPluginCallPeerApi*>(parameters);
+    const OrthancPeers& peers = *reinterpret_cast<const OrthancPeers*>(p.peers);
+
+    HttpClient client(peers.GetPeerParameters(p.peerIndex), p.uri);
+    client.SetConvertHeadersToLowerCase(false);
+
+    if (p.timeout != 0)
+    {
+      client.SetTimeout(p.timeout);
+    }
+
+    for (uint32_t i = 0; i < p.additionalHeadersCount; i++)
+    {
+      if (p.additionalHeadersKeys[i] == NULL ||
+          p.additionalHeadersValues[i] == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+
+      client.AddHeader(p.additionalHeadersKeys[i], p.additionalHeadersValues[i]);
+    }
+
+    switch (p.method)
+    {
+      case OrthancPluginHttpMethod_Get:
+        client.SetMethod(HttpMethod_Get);
+        break;
+
+      case OrthancPluginHttpMethod_Post:
+        client.SetMethod(HttpMethod_Post);
+        client.GetBody().assign(p.body, p.bodySize);
+        break;
+
+      case OrthancPluginHttpMethod_Put:
+        client.SetMethod(HttpMethod_Put);
+        client.GetBody().assign(p.body, p.bodySize);
+        break;
+
+      case OrthancPluginHttpMethod_Delete:
+        client.SetMethod(HttpMethod_Delete);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    std::string body;
+    HttpClient::HttpHeaders headers;
+
+    bool success = client.Apply(body, headers);
+
+    // The HTTP request has succeeded
+    *p.httpStatus = static_cast<uint16_t>(client.GetLastStatus());
+
+    if (!success)
+    {
+      HttpClient::ThrowException(client.GetLastStatus());
+    }
+
+    // Copy the HTTP headers of the answer, if the plugin requested them
+    if (p.answerHeaders != NULL)
+    {
+      Json::Value json = Json::objectValue;
+
+      for (HttpClient::HttpHeaders::const_iterator 
+             it = headers.begin(); it != headers.end(); ++it)
+      {
+        json[it->first] = it->second;
+      }
+        
+      std::string s = json.toStyledString();
+      CopyToMemoryBuffer(*p.answerHeaders, s);
+    }
+
+    // Copy the body of the answer if it makes sense
+    if (p.method != OrthancPluginHttpMethod_Delete)
+    {
+      CopyToMemoryBuffer(*p.answerBody, body);
+    }
+  }
+
+
   void OrthancPlugins::ConvertPixelFormat(const void* parameters)
   {
     const _OrthancPluginConvertPixelFormat& p = *reinterpret_cast<const _OrthancPluginConvertPixelFormat*>(parameters);
     const ImageAccessor& source = *reinterpret_cast<const ImageAccessor*>(p.source);
 
-    std::auto_ptr<ImageAccessor> target(new Image(Plugins::Convert(p.targetFormat), source.GetWidth(), source.GetHeight()));
+    std::auto_ptr<ImageAccessor> target(new Image(Plugins::Convert(p.targetFormat), source.GetWidth(), source.GetHeight(), false));
     ImageProcessing::Convert(*target, source);
 
-    *(p.target) = reinterpret_cast<OrthancPluginImage*>(target.release());
+    *(p.target) = ReturnImage(target);
   }
 
 
@@ -1264,43 +2149,243 @@
     {
       if (p.instanceId == NULL)
       {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
+        throw OrthancException(ErrorCode_NullPointer);
       }
 
       std::string content;
-      pimpl_->context_->ReadFile(content, p.instanceId, FileContentType_Dicom);
+
+      {
+        PImpl::ServerContextLock lock(*pimpl_);
+        lock.GetContext().ReadDicom(content, p.instanceId);
+      }
+
       dicom.reset(new ParsedDicomFile(content));
     }
 
     Json::Value json;
-    dicom->ToJson(json, Plugins::Convert(p.format), 
-                  static_cast<DicomToJsonFlags>(p.flags), p.maxStringLength);
+    dicom->DatasetToJson(json, Plugins::Convert(p.format), 
+                         static_cast<DicomToJsonFlags>(p.flags), p.maxStringLength);
 
     Json::FastWriter writer;
     *p.result = CopyString(writer.write(json));
   }
         
 
-  bool OrthancPlugins::InvokeService(SharedLibrary& plugin,
-                                     _OrthancPluginService service,
-                                     const void* parameters)
+  void OrthancPlugins::ApplyCreateDicom(_OrthancPluginService service,
+                                        const void* parameters)
+  {
+    const _OrthancPluginCreateDicom& p =
+      *reinterpret_cast<const _OrthancPluginCreateDicom*>(parameters);
+
+    Json::Value json;
+
+    if (p.json == NULL)
+    {
+      json = Json::objectValue;
+    }
+    else
+    {
+      Json::Reader reader;
+      if (!reader.parse(p.json, json))
+      {
+        throw OrthancException(ErrorCode_BadJson);
+      }
+    }
+
+    std::string dicom;
+
+    {
+      std::auto_ptr<ParsedDicomFile> file
+        (ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(p.flags)));
+
+      if (p.pixelData)
+      {
+        file->EmbedImage(*reinterpret_cast<const ImageAccessor*>(p.pixelData));
+      }
+
+      file->SaveToMemoryBuffer(dicom);
+    }
+
+    CopyToMemoryBuffer(*p.target, dicom);
+  }
+
+
+  void OrthancPlugins::ComputeHash(_OrthancPluginService service,
+                                   const void* parameters)
+  {
+    const _OrthancPluginComputeHash& p =
+      *reinterpret_cast<const _OrthancPluginComputeHash*>(parameters);
+ 
+    std::string hash;
+    switch (service)
+    {
+      case _OrthancPluginService_ComputeMd5:
+        Toolbox::ComputeMD5(hash, p.buffer, p.size);
+        break;
+
+      case _OrthancPluginService_ComputeSha1:
+        Toolbox::ComputeSHA1(hash, p.buffer, p.size);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+   
+    *p.result = CopyString(hash);
+  }
+
+
+  void OrthancPlugins::ApplyCreateImage(_OrthancPluginService service,
+                                        const void* parameters)
+  {
+    const _OrthancPluginCreateImage& p =
+      *reinterpret_cast<const _OrthancPluginCreateImage*>(parameters);
+
+    std::auto_ptr<ImageAccessor> result;
+
+    switch (service)
+    {
+      case _OrthancPluginService_CreateImage:
+        result.reset(new Image(Plugins::Convert(p.format), p.width, p.height, false));
+        break;
+
+      case _OrthancPluginService_CreateImageAccessor:
+        result.reset(new ImageAccessor);
+        result->AssignWritable(Plugins::Convert(p.format), p.width, p.height, p.pitch, p.buffer);
+        break;
+
+      case _OrthancPluginService_DecodeDicomImage:
+      {
+        result.reset(Decode(p.constBuffer, p.bufferSize, p.frameIndex));
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    *(p.target) = ReturnImage(result);
+  }
+
+
+  void OrthancPlugins::ApplySendMultipartItem(const void* parameters)
   {
-    VLOG(1) << "Calling service " << service << " from plugin " << plugin.GetPath();
-
-    boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
+    // An exception might be raised in this function if the
+    // connection was closed by the HTTP client.
+    const _OrthancPluginAnswerBuffer& p =
+      *reinterpret_cast<const _OrthancPluginAnswerBuffer*>(parameters);
+
+    HttpOutput* output = reinterpret_cast<HttpOutput*>(p.output);
+
+    std::map<std::string, std::string> headers;  // No custom headers
+    output->SendMultipartItem(p.answer, p.answerSize, headers);
+  }
+
+
+  void OrthancPlugins::ApplySendMultipartItem2(const void* parameters)
+  {
+    // An exception might be raised in this function if the
+    // connection was closed by the HTTP client.
+    const _OrthancPluginSendMultipartItem2& p =
+      *reinterpret_cast<const _OrthancPluginSendMultipartItem2*>(parameters);
+    HttpOutput* output = reinterpret_cast<HttpOutput*>(p.output);
+    
+    std::map<std::string, std::string> headers;
+    for (uint32_t i = 0; i < p.headersCount; i++)
+    {
+      headers[p.headersKeys[i]] = p.headersValues[i];
+    }
+    
+    output->SendMultipartItem(p.answer, p.answerSize, headers);
+  }
+      
+
+  void OrthancPlugins::DatabaseAnswer(const void* parameters)
+  {
+    const _OrthancPluginDatabaseAnswer& p =
+      *reinterpret_cast<const _OrthancPluginDatabaseAnswer*>(parameters);
+
+    if (pimpl_->database_.get() != NULL)
+    {
+      pimpl_->database_->AnswerReceived(p);
+    }
+    else
+    {
+      LOG(ERROR) << "Cannot invoke this service without a custom database back-end";
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+  }
+
+
+  namespace
+  {
+    class DictionaryReadLocker
+    {
+    private:
+      const DcmDataDictionary& dictionary_;
+
+    public:
+      DictionaryReadLocker() : dictionary_(dcmDataDict.rdlock())
+      {
+      }
+
+      ~DictionaryReadLocker()
+      {
+        dcmDataDict.unlock();
+      }
+
+      const DcmDataDictionary* operator->()
+      {
+        return &dictionary_;
+      }
+    };
+  }
+
+
+  void OrthancPlugins::ApplyLookupDictionary(const void* parameters)
+  {
+    const _OrthancPluginLookupDictionary& p =
+      *reinterpret_cast<const _OrthancPluginLookupDictionary*>(parameters);
+
+    DicomTag tag(FromDcmtkBridge::ParseTag(p.name));
+    DcmTagKey tag2(tag.GetGroup(), tag.GetElement());
+
+    DictionaryReadLocker locker;
+    const DcmDictEntry* entry = locker->findEntry(tag2, NULL);
+
+    if (entry == NULL)
+    {
+      throw OrthancException(ErrorCode_UnknownDicomTag);
+    }
+    else
+    {
+      p.target->group = entry->getKey().getGroup();
+      p.target->element = entry->getKey().getElement();
+      p.target->vr = Plugins::Convert(FromDcmtkBridge::Convert(entry->getEVR()));
+      p.target->minMultiplicity = static_cast<uint32_t>(entry->getVMMin());
+      p.target->maxMultiplicity = (entry->getVMMax() == DcmVariableVM ? 0 : static_cast<uint32_t>(entry->getVMMax()));
+    }
+  }
+
+
+  bool OrthancPlugins::InvokeSafeService(SharedLibrary& plugin,
+                                         _OrthancPluginService service,
+                                         const void* parameters)
+  {
+    // Services that can be run without mutual exclusion
 
     switch (service)
     {
       case _OrthancPluginService_GetOrthancPath:
       {
-        std::string s = Toolbox::GetPathToExecutable();
+        std::string s = SystemToolbox::GetPathToExecutable();
         *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
         return true;
       }
 
       case _OrthancPluginService_GetOrthancDirectory:
       {
-        std::string s = Toolbox::GetDirectoryOfExecutable();
+        std::string s = SystemToolbox::GetDirectoryOfExecutable();
         *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
         return true;
       }
@@ -1325,22 +2410,6 @@
         BufferCompression(parameters);
         return true;
 
-      case _OrthancPluginService_RegisterRestCallback:
-        RegisterRestCallback(parameters, true);
-        return true;
-
-      case _OrthancPluginService_RegisterRestCallbackNoLock:
-        RegisterRestCallback(parameters, false);
-        return true;
-
-      case _OrthancPluginService_RegisterOnStoredInstanceCallback:
-        RegisterOnStoredInstanceCallback(parameters);
-        return true;
-
-      case _OrthancPluginService_RegisterOnChangeCallback:
-        RegisterOnChangeCallback(parameters);
-        return true;
-
       case _OrthancPluginService_AnswerBuffer:
         AnswerBuffer(parameters);
         return true;
@@ -1365,6 +2434,10 @@
         RestApiGet(parameters, true);
         return true;
 
+      case _OrthancPluginService_RestApiGet2:
+        RestApiGet2(parameters);
+        return true;
+
       case _OrthancPluginService_RestApiPost:
         RestApiPostPut(true, parameters, false);
         return true;
@@ -1432,9 +2505,538 @@
       case _OrthancPluginService_GetInstanceSimplifiedJson:
       case _OrthancPluginService_HasInstanceMetadata:
       case _OrthancPluginService_GetInstanceMetadata:
+      case _OrthancPluginService_GetInstanceOrigin:
         AccessDicomInstance(service, parameters);
         return true;
 
+      case _OrthancPluginService_SetGlobalProperty:
+      {
+        const _OrthancPluginGlobalProperty& p = 
+          *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters);
+        if (p.property < 1024)
+        {
+          return false;
+        }
+        else
+        {
+          PImpl::ServerContextLock lock(*pimpl_);
+          lock.GetContext().GetIndex().SetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value);
+          return true;
+        }
+      }
+
+      case _OrthancPluginService_GetGlobalProperty:
+      {
+        const _OrthancPluginGlobalProperty& p = 
+          *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters);
+
+        std::string result;
+
+        {
+          PImpl::ServerContextLock lock(*pimpl_);
+          result = lock.GetContext().GetIndex().GetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value);
+        }
+
+        *(p.result) = CopyString(result);
+        return true;
+      }
+
+      case _OrthancPluginService_GetExpectedDatabaseVersion:
+      {
+        const _OrthancPluginReturnSingleValue& p =
+          *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters);
+        *(p.resultUint32) = ORTHANC_DATABASE_VERSION;
+        return true;
+      }
+
+      case _OrthancPluginService_StartMultipartAnswer:
+      {
+        const _OrthancPluginStartMultipartAnswer& p =
+          *reinterpret_cast<const _OrthancPluginStartMultipartAnswer*>(parameters);
+        HttpOutput* output = reinterpret_cast<HttpOutput*>(p.output);
+        output->StartMultipart(p.subType, p.contentType);
+        return true;
+      }
+
+      case _OrthancPluginService_SendMultipartItem:
+        ApplySendMultipartItem(parameters);
+        return true;
+
+      case _OrthancPluginService_SendMultipartItem2:
+        ApplySendMultipartItem2(parameters);
+        return true;
+
+      case _OrthancPluginService_ReadFile:
+      {
+        const _OrthancPluginReadFile& p =
+          *reinterpret_cast<const _OrthancPluginReadFile*>(parameters);
+
+        std::string content;
+        SystemToolbox::ReadFile(content, p.path);
+        CopyToMemoryBuffer(*p.target, content.size() > 0 ? content.c_str() : NULL, content.size());
+
+        return true;
+      }
+
+      case _OrthancPluginService_WriteFile:
+      {
+        const _OrthancPluginWriteFile& p =
+          *reinterpret_cast<const _OrthancPluginWriteFile*>(parameters);
+        SystemToolbox::WriteFile(p.data, p.size, p.path);
+        return true;
+      }
+
+      case _OrthancPluginService_GetErrorDescription:
+      {
+        const _OrthancPluginGetErrorDescription& p =
+          *reinterpret_cast<const _OrthancPluginGetErrorDescription*>(parameters);
+        *(p.target) = EnumerationToString(static_cast<ErrorCode>(p.error));
+        return true;
+      }
+
+      case _OrthancPluginService_GetImagePixelFormat:
+      {
+        const _OrthancPluginGetImageInfo& p = *reinterpret_cast<const _OrthancPluginGetImageInfo*>(parameters);
+        *(p.resultPixelFormat) = Plugins::Convert(reinterpret_cast<const ImageAccessor*>(p.image)->GetFormat());
+        return true;
+      }
+
+      case _OrthancPluginService_GetImageWidth:
+      {
+        const _OrthancPluginGetImageInfo& p = *reinterpret_cast<const _OrthancPluginGetImageInfo*>(parameters);
+        *(p.resultUint32) = reinterpret_cast<const ImageAccessor*>(p.image)->GetWidth();
+        return true;
+      }
+
+      case _OrthancPluginService_GetImageHeight:
+      {
+        const _OrthancPluginGetImageInfo& p = *reinterpret_cast<const _OrthancPluginGetImageInfo*>(parameters);
+        *(p.resultUint32) = reinterpret_cast<const ImageAccessor*>(p.image)->GetHeight();
+        return true;
+      }
+
+      case _OrthancPluginService_GetImagePitch:
+      {
+        const _OrthancPluginGetImageInfo& p = *reinterpret_cast<const _OrthancPluginGetImageInfo*>(parameters);
+        *(p.resultUint32) = reinterpret_cast<const ImageAccessor*>(p.image)->GetPitch();
+        return true;
+      }
+
+      case _OrthancPluginService_GetImageBuffer:
+      {
+        const _OrthancPluginGetImageInfo& p = *reinterpret_cast<const _OrthancPluginGetImageInfo*>(parameters);
+        *(p.resultBuffer) = reinterpret_cast<const ImageAccessor*>(p.image)->GetBuffer();
+        return true;
+      }
+
+      case _OrthancPluginService_FreeImage:
+      {
+        const _OrthancPluginFreeImage& p = *reinterpret_cast<const _OrthancPluginFreeImage*>(parameters);
+
+        if (p.image != NULL)
+        {
+          delete reinterpret_cast<ImageAccessor*>(p.image);
+        }
+
+        return true;
+      }
+
+      case _OrthancPluginService_UncompressImage:
+        UncompressImage(parameters);
+        return true;
+
+      case _OrthancPluginService_CompressImage:
+        CompressImage(parameters);
+        return true;
+
+      case _OrthancPluginService_CallHttpClient:
+        CallHttpClient(parameters);
+        return true;
+
+      case _OrthancPluginService_CallHttpClient2:
+        CallHttpClient2(parameters);
+        return true;
+
+      case _OrthancPluginService_ConvertPixelFormat:
+        ConvertPixelFormat(parameters);
+        return true;
+
+      case _OrthancPluginService_GetFontsCount:
+      {
+        const _OrthancPluginReturnSingleValue& p =
+          *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters);
+        *(p.resultUint32) = Configuration::GetFontRegistry().GetSize();
+        return true;
+      }
+
+      case _OrthancPluginService_GetFontInfo:
+        GetFontInfo(parameters);
+        return true;
+
+      case _OrthancPluginService_DrawText:
+        DrawText(parameters);
+        return true;
+
+      case _OrthancPluginService_StorageAreaCreate:
+      {
+        const _OrthancPluginStorageAreaCreate& p =
+          *reinterpret_cast<const _OrthancPluginStorageAreaCreate*>(parameters);
+        IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
+        storage.Create(p.uuid, p.content, static_cast<size_t>(p.size), Plugins::Convert(p.type));
+        return true;
+      }
+
+      case _OrthancPluginService_StorageAreaRead:
+      {
+        const _OrthancPluginStorageAreaRead& p =
+          *reinterpret_cast<const _OrthancPluginStorageAreaRead*>(parameters);
+        IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
+        std::string content;
+        storage.Read(content, p.uuid, Plugins::Convert(p.type));
+        CopyToMemoryBuffer(*p.target, content);
+        return true;
+      }
+
+      case _OrthancPluginService_StorageAreaRemove:
+      {
+        const _OrthancPluginStorageAreaRemove& p =
+          *reinterpret_cast<const _OrthancPluginStorageAreaRemove*>(parameters);
+        IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
+        storage.Remove(p.uuid, Plugins::Convert(p.type));
+        return true;
+      }
+
+      case _OrthancPluginService_DicomBufferToJson:
+      case _OrthancPluginService_DicomInstanceToJson:
+        ApplyDicomToJson(service, parameters);
+        return true;
+
+      case _OrthancPluginService_CreateDicom:
+        ApplyCreateDicom(service, parameters);
+        return true;
+
+      case _OrthancPluginService_WorklistAddAnswer:
+      {
+        const _OrthancPluginWorklistAnswersOperation& p =
+          *reinterpret_cast<const _OrthancPluginWorklistAnswersOperation*>(parameters);
+        reinterpret_cast<const WorklistHandler*>(p.query)->AddAnswer(p.answers, p.dicom, p.size);
+        return true;
+      }
+
+      case _OrthancPluginService_WorklistMarkIncomplete:
+      {
+        const _OrthancPluginWorklistAnswersOperation& p =
+          *reinterpret_cast<const _OrthancPluginWorklistAnswersOperation*>(parameters);
+        reinterpret_cast<DicomFindAnswers*>(p.answers)->SetComplete(false);
+        return true;
+      }
+
+      case _OrthancPluginService_WorklistIsMatch:
+      {
+        const _OrthancPluginWorklistQueryOperation& p =
+          *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters);
+        *p.isMatch = reinterpret_cast<const WorklistHandler*>(p.query)->IsMatch(p.dicom, p.size);
+        return true;
+      }
+
+      case _OrthancPluginService_WorklistGetDicomQuery:
+      {
+        const _OrthancPluginWorklistQueryOperation& p =
+          *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters);
+        reinterpret_cast<const WorklistHandler*>(p.query)->GetDicomQuery(*p.target);
+        return true;
+      }
+
+      case _OrthancPluginService_FindAddAnswer:
+      {
+        const _OrthancPluginFindOperation& p =
+          *reinterpret_cast<const _OrthancPluginFindOperation*>(parameters);
+        reinterpret_cast<DicomFindAnswers*>(p.answers)->Add(p.dicom, p.size);
+        return true;
+      }
+
+      case _OrthancPluginService_FindMarkIncomplete:
+      {
+        const _OrthancPluginFindOperation& p =
+          *reinterpret_cast<const _OrthancPluginFindOperation*>(parameters);
+        reinterpret_cast<DicomFindAnswers*>(p.answers)->SetComplete(false);
+        return true;
+      }
+
+      case _OrthancPluginService_GetFindQuerySize:
+      case _OrthancPluginService_GetFindQueryTag:
+      case _OrthancPluginService_GetFindQueryTagName:
+      case _OrthancPluginService_GetFindQueryValue:
+      {
+        const _OrthancPluginFindOperation& p =
+          *reinterpret_cast<const _OrthancPluginFindOperation*>(parameters);
+        reinterpret_cast<const FindHandler*>(p.query)->Invoke(service, p);
+        return true;
+      }
+
+      case _OrthancPluginService_CreateImage:
+      case _OrthancPluginService_CreateImageAccessor:
+      case _OrthancPluginService_DecodeDicomImage:
+        ApplyCreateImage(service, parameters);
+        return true;
+
+      case _OrthancPluginService_ComputeMd5:
+      case _OrthancPluginService_ComputeSha1:
+        ComputeHash(service, parameters);
+        return true;
+
+      case _OrthancPluginService_LookupDictionary:
+        ApplyLookupDictionary(parameters);
+        return true;
+
+      case _OrthancPluginService_GenerateUuid:
+      {
+        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = 
+          CopyString(Toolbox::GenerateUuid());
+        return true;
+      }
+
+      case _OrthancPluginService_CreateFindMatcher:
+      {
+        const _OrthancPluginCreateFindMatcher& p =
+          *reinterpret_cast<const _OrthancPluginCreateFindMatcher*>(parameters);
+        ParsedDicomFile query(p.query, p.size);
+        *(p.target) = reinterpret_cast<OrthancPluginFindMatcher*>(new HierarchicalMatcher(query));
+        return true;
+      }
+
+      case _OrthancPluginService_FreeFindMatcher:
+      {
+        const _OrthancPluginFreeFindMatcher& p =
+          *reinterpret_cast<const _OrthancPluginFreeFindMatcher*>(parameters);
+
+        if (p.matcher != NULL)
+        {
+          delete reinterpret_cast<HierarchicalMatcher*>(p.matcher);
+        }
+
+        return true;
+      }
+
+      case _OrthancPluginService_FindMatcherIsMatch:
+      {
+        const _OrthancPluginFindMatcherIsMatch& p =
+          *reinterpret_cast<const _OrthancPluginFindMatcherIsMatch*>(parameters);
+
+        if (p.matcher == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+        else
+        {
+          ParsedDicomFile query(p.dicom, p.size);
+          *p.isMatch = reinterpret_cast<const HierarchicalMatcher*>(p.matcher)->Match(query) ? 1 : 0;
+          return true;
+        }
+      }
+
+      case _OrthancPluginService_GetPeers:
+      {
+        const _OrthancPluginGetPeers& p =
+          *reinterpret_cast<const _OrthancPluginGetPeers*>(parameters);
+        *(p.peers) = reinterpret_cast<OrthancPluginPeers*>(new OrthancPeers);
+        return true;
+      }
+
+      case _OrthancPluginService_FreePeers:
+      {
+        const _OrthancPluginFreePeers& p =
+          *reinterpret_cast<const _OrthancPluginFreePeers*>(parameters);
+
+        if (p.peers != NULL)
+        {
+          delete reinterpret_cast<OrthancPeers*>(p.peers);
+        }
+        
+        return true;
+      }
+
+      case _OrthancPluginService_GetPeersCount:
+      {
+        const _OrthancPluginGetPeersCount& p =
+          *reinterpret_cast<const _OrthancPluginGetPeersCount*>(parameters);
+
+        if (p.peers == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+        else
+        {
+          *(p.target) = reinterpret_cast<const OrthancPeers*>(p.peers)->GetPeersCount();
+          return true;
+        }
+      }
+
+      case _OrthancPluginService_GetPeerName:
+      {
+        const _OrthancPluginGetPeerProperty& p =
+          *reinterpret_cast<const _OrthancPluginGetPeerProperty*>(parameters);
+
+        if (p.peers == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+        else
+        {
+          *(p.target) = reinterpret_cast<const OrthancPeers*>(p.peers)->GetPeerName(p.peerIndex).c_str();
+          return true;
+        }
+      }
+
+      case _OrthancPluginService_GetPeerUrl:
+      {
+        const _OrthancPluginGetPeerProperty& p =
+          *reinterpret_cast<const _OrthancPluginGetPeerProperty*>(parameters);
+
+        if (p.peers == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+        else
+        {
+          *(p.target) = reinterpret_cast<const OrthancPeers*>(p.peers)->GetPeerParameters(p.peerIndex).GetUrl().c_str();
+          return true;
+        }
+      }
+
+      case _OrthancPluginService_GetPeerUserProperty:
+      {
+        const _OrthancPluginGetPeerProperty& p =
+          *reinterpret_cast<const _OrthancPluginGetPeerProperty*>(parameters);
+
+        if (p.peers == NULL ||
+            p.userProperty == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+        else
+        {
+          const WebServiceParameters::Dictionary& properties = 
+            reinterpret_cast<const OrthancPeers*>(p.peers)->GetPeerParameters(p.peerIndex).GetUserProperties();
+
+          WebServiceParameters::Dictionary::const_iterator found =
+            properties.find(p.userProperty);
+
+          if (found == properties.end())
+          {
+            *(p.target) = NULL;
+          }
+          else
+          {
+            *(p.target) = found->second.c_str();
+          }
+
+          return true;
+        }
+      }
+
+      case _OrthancPluginService_CallPeerApi:
+        CallPeerApi(parameters);
+        return true;
+
+      case _OrthancPluginService_CreateJob:
+      {
+        const _OrthancPluginCreateJob& p =
+          *reinterpret_cast<const _OrthancPluginCreateJob*>(parameters);
+        *(p.target) = reinterpret_cast<OrthancPluginJob*>(new PluginsJob(p));
+        return true;
+      }
+
+      case _OrthancPluginService_FreeJob:
+      {
+        const _OrthancPluginFreeJob& p =
+          *reinterpret_cast<const _OrthancPluginFreeJob*>(parameters);
+
+        if (p.job != NULL)
+        {
+          delete reinterpret_cast<PluginsJob*>(p.job);
+        }
+
+        return true;
+      }
+
+      case _OrthancPluginService_SubmitJob:
+      {
+        const _OrthancPluginSubmitJob& p =
+          *reinterpret_cast<const _OrthancPluginSubmitJob*>(parameters);
+
+        std::string uuid;
+
+        PImpl::ServerContextLock lock(*pimpl_);
+        lock.GetContext().GetJobsEngine().GetRegistry().Submit
+          (uuid, reinterpret_cast<PluginsJob*>(p.job), p.priority);
+        
+        *p.resultId = CopyString(uuid);
+
+        return true;
+      }
+
+      default:
+        return false;
+    }
+  }
+
+
+
+  bool OrthancPlugins::InvokeProtectedService(SharedLibrary& plugin,
+                                              _OrthancPluginService service,
+                                              const void* parameters)
+  {
+    // Services that must be run in mutual exclusion. Guideline:
+    // Whenever "pimpl_" is directly accessed by the service, it
+    // should be listed here.
+    
+    switch (service)
+    {
+      case _OrthancPluginService_RegisterRestCallback:
+        RegisterRestCallback(parameters, true);
+        return true;
+
+      case _OrthancPluginService_RegisterRestCallbackNoLock:
+        RegisterRestCallback(parameters, false);
+        return true;
+
+      case _OrthancPluginService_RegisterOnStoredInstanceCallback:
+        RegisterOnStoredInstanceCallback(parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterOnChangeCallback:
+        RegisterOnChangeCallback(parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterWorklistCallback:
+        RegisterWorklistCallback(parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterFindCallback:
+        RegisterFindCallback(parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterMoveCallback:
+        RegisterMoveCallback(parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterDecodeImageCallback:
+        RegisterDecodeImageCallback(parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterJobsUnserializer:
+        RegisterJobsUnserializer(parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterIncomingHttpRequestFilter:
+        RegisterIncomingHttpRequestFilter(parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterIncomingHttpRequestFilter2:
+        RegisterIncomingHttpRequestFilter2(parameters);
+        return true;
+
       case _OrthancPluginService_RegisterStorageArea:
       {
         LOG(INFO) << "Plugin has registered a custom storage area";
@@ -1461,33 +3063,6 @@
         return true;
       }
 
-      case _OrthancPluginService_SetGlobalProperty:
-      {
-        const _OrthancPluginGlobalProperty& p = 
-          *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters);
-        if (p.property < 1024)
-        {
-          return false;
-        }
-        else
-        {
-          CheckContextAvailable();
-          pimpl_->context_->GetIndex().SetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value);
-          return true;
-        }
-      }
-
-      case _OrthancPluginService_GetGlobalProperty:
-      {
-        CheckContextAvailable();
-
-        const _OrthancPluginGlobalProperty& p = 
-          *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters);
-        std::string result = pimpl_->context_->GetIndex().GetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value);
-        *(p.result) = CopyString(result);
-        return true;
-      }
-
       case _OrthancPluginService_GetCommandLineArgumentsCount:
       {
         const _OrthancPluginReturnSingleValue& p =
@@ -1559,187 +3134,7 @@
       }
 
       case _OrthancPluginService_DatabaseAnswer:
-      {
-        const _OrthancPluginDatabaseAnswer& p =
-          *reinterpret_cast<const _OrthancPluginDatabaseAnswer*>(parameters);
-
-        if (pimpl_->database_.get() != NULL)
-        {
-          pimpl_->database_->AnswerReceived(p);
-          return true;
-        }
-        else
-        {
-          LOG(ERROR) << "Cannot invoke this service without a custom database back-end";
-          throw OrthancException(ErrorCode_BadRequest);
-        }
-      }
-
-      case _OrthancPluginService_GetExpectedDatabaseVersion:
-      {
-        const _OrthancPluginReturnSingleValue& p =
-          *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters);
-        *(p.resultUint32) = ORTHANC_DATABASE_VERSION;
-        return true;
-      }
-
-      case _OrthancPluginService_StartMultipartAnswer:
-      {
-        const _OrthancPluginStartMultipartAnswer& p =
-          *reinterpret_cast<const _OrthancPluginStartMultipartAnswer*>(parameters);
-        HttpOutput* output = reinterpret_cast<HttpOutput*>(p.output);
-        output->StartMultipart(p.subType, p.contentType);
-        return true;
-      }
-
-      case _OrthancPluginService_SendMultipartItem:
-      {
-        // An exception might be raised in this function if the
-        // connection was closed by the HTTP client.
-        const _OrthancPluginAnswerBuffer& p =
-          *reinterpret_cast<const _OrthancPluginAnswerBuffer*>(parameters);
-        HttpOutput* output = reinterpret_cast<HttpOutput*>(p.output);
-        output->SendMultipartItem(p.answer, p.answerSize);
-        return true;
-      }
-
-      case _OrthancPluginService_ReadFile:
-      {
-        const _OrthancPluginReadFile& p =
-          *reinterpret_cast<const _OrthancPluginReadFile*>(parameters);
-
-        std::string content;
-        Toolbox::ReadFile(content, p.path);
-        CopyToMemoryBuffer(*p.target, content.size() > 0 ? content.c_str() : NULL, content.size());
-
-        return true;
-      }
-
-      case _OrthancPluginService_WriteFile:
-      {
-        const _OrthancPluginWriteFile& p =
-          *reinterpret_cast<const _OrthancPluginWriteFile*>(parameters);
-        Toolbox::WriteFile(p.data, p.size, p.path);
-        return true;
-      }
-
-      case _OrthancPluginService_GetErrorDescription:
-      {
-        const _OrthancPluginGetErrorDescription& p =
-          *reinterpret_cast<const _OrthancPluginGetErrorDescription*>(parameters);
-        *(p.target) = EnumerationToString(static_cast<ErrorCode>(p.error));
-        return true;
-      }
-
-      case _OrthancPluginService_GetImagePixelFormat:
-      {
-        const _OrthancPluginGetImageInfo& p = *reinterpret_cast<const _OrthancPluginGetImageInfo*>(parameters);
-        *(p.resultPixelFormat) = Plugins::Convert(reinterpret_cast<const ImageAccessor*>(p.image)->GetFormat());
-        return true;
-      }
-
-      case _OrthancPluginService_GetImageWidth:
-      {
-        const _OrthancPluginGetImageInfo& p = *reinterpret_cast<const _OrthancPluginGetImageInfo*>(parameters);
-        *(p.resultUint32) = reinterpret_cast<const ImageAccessor*>(p.image)->GetWidth();
-        return true;
-      }
-
-      case _OrthancPluginService_GetImageHeight:
-      {
-        const _OrthancPluginGetImageInfo& p = *reinterpret_cast<const _OrthancPluginGetImageInfo*>(parameters);
-        *(p.resultUint32) = reinterpret_cast<const ImageAccessor*>(p.image)->GetHeight();
-        return true;
-      }
-
-      case _OrthancPluginService_GetImagePitch:
-      {
-        const _OrthancPluginGetImageInfo& p = *reinterpret_cast<const _OrthancPluginGetImageInfo*>(parameters);
-        *(p.resultUint32) = reinterpret_cast<const ImageAccessor*>(p.image)->GetPitch();
-        return true;
-      }
-
-      case _OrthancPluginService_GetImageBuffer:
-      {
-        const _OrthancPluginGetImageInfo& p = *reinterpret_cast<const _OrthancPluginGetImageInfo*>(parameters);
-        *(p.resultBuffer) = reinterpret_cast<const ImageAccessor*>(p.image)->GetConstBuffer();
-        return true;
-      }
-
-      case _OrthancPluginService_FreeImage:
-      {
-        const _OrthancPluginFreeImage& p = *reinterpret_cast<const _OrthancPluginFreeImage*>(parameters);
-        if (p.image == NULL)
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
-        else
-        {
-          delete reinterpret_cast<ImageAccessor*>(p.image);
-          return true;
-        }
-      }
-
-      case _OrthancPluginService_UncompressImage:
-        UncompressImage(parameters);
-        return true;
-
-      case _OrthancPluginService_CompressImage:
-        CompressImage(parameters);
-        return true;
-
-      case _OrthancPluginService_CallHttpClient:
-        CallHttpClient(parameters);
-        return true;
-
-      case _OrthancPluginService_ConvertPixelFormat:
-        ConvertPixelFormat(parameters);
-        return true;
-
-      case _OrthancPluginService_GetFontsCount:
-      {
-        const _OrthancPluginReturnSingleValue& p =
-          *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters);
-        *(p.resultUint32) = Configuration::GetFontRegistry().GetSize();
-        return true;
-      }
-
-      case _OrthancPluginService_GetFontInfo:
-        GetFontInfo(parameters);
-        return true;
-
-      case _OrthancPluginService_DrawText:
-        DrawText(parameters);
-        return true;
-
-      case _OrthancPluginService_StorageAreaCreate:
-      {
-        const _OrthancPluginStorageAreaCreate& p =
-          *reinterpret_cast<const _OrthancPluginStorageAreaCreate*>(parameters);
-        IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
-        storage.Create(p.uuid, p.content, p.size, Plugins::Convert(p.type));
-        return true;
-      }
-
-      case _OrthancPluginService_StorageAreaRead:
-      {
-        const _OrthancPluginStorageAreaRead& p =
-          *reinterpret_cast<const _OrthancPluginStorageAreaRead*>(parameters);
-        IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
-        std::string content;
-        storage.Read(content, p.uuid, Plugins::Convert(p.type));
-        CopyToMemoryBuffer(*p.target, content);
-        return true;
-      }
-
-      case _OrthancPluginService_StorageAreaRemove:
-      {
-        const _OrthancPluginStorageAreaRemove& p =
-          *reinterpret_cast<const _OrthancPluginStorageAreaRemove*>(parameters);
-        IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
-        storage.Remove(p.uuid, Plugins::Convert(p.type));
-        return true;
-      }
+        throw OrthancException(ErrorCode_InternalError);   // Implemented before locking (*)
 
       case _OrthancPluginService_RegisterErrorCode:
       {
@@ -1755,7 +3150,17 @@
           *reinterpret_cast<const _OrthancPluginRegisterDictionaryTag*>(parameters);
         FromDcmtkBridge::RegisterDictionaryTag(DicomTag(p.group, p.element),
                                                Plugins::Convert(p.vr), p.name,
-                                               p.minMultiplicity, p.maxMultiplicity);
+                                               p.minMultiplicity, p.maxMultiplicity, "");
+        return true;
+      }
+
+      case _OrthancPluginService_RegisterPrivateDictionaryTag:
+      {
+        const _OrthancPluginRegisterPrivateDictionaryTag& p =
+          *reinterpret_cast<const _OrthancPluginRegisterPrivateDictionaryTag*>(parameters);
+        FromDcmtkBridge::RegisterDictionaryTag(DicomTag(p.group, p.element),
+                                               Plugins::Convert(p.vr), p.name,
+                                               p.minMultiplicity, p.maxMultiplicity, p.privateCreator);
         return true;
       }
 
@@ -1771,16 +3176,11 @@
         }
 
         IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
-        Toolbox::ReconstructMainDicomTags(*pimpl_->database_, storage, Plugins::Convert(p.level));
+        ServerToolbox::ReconstructMainDicomTags(*pimpl_->database_, storage, Plugins::Convert(p.level));
 
         return true;
       }
 
-      case _OrthancPluginService_DicomBufferToJson:
-      case _OrthancPluginService_DicomInstanceToJson:
-        ApplyDicomToJson(service, parameters);
-        return true;
-
       default:
       {
         // This service is unknown to the Orthanc plugin engine
@@ -1790,13 +3190,48 @@
   }
 
 
+
+  bool OrthancPlugins::InvokeService(SharedLibrary& plugin,
+                                     _OrthancPluginService service,
+                                     const void* parameters)
+  {
+    VLOG(1) << "Calling service " << service << " from plugin " << plugin.GetPath();
+
+    if (service == _OrthancPluginService_DatabaseAnswer)
+    {
+      // This case solves a deadlock at (*) reported by James Webster
+      // on 2015-10-27 that was present in versions of Orthanc <=
+      // 0.9.4 and related to database plugins implementing a custom
+      // index. The problem was that locking the database is already
+      // ensured by the "ServerIndex" class if the invoked service is
+      // "DatabaseAnswer".
+      DatabaseAnswer(parameters);
+      return true;
+    }
+
+    if (InvokeSafeService(plugin, service, parameters))
+    {
+      // The invoked service does not require locking
+      return true;
+    }
+    else
+    {
+      // The invoked service requires locking
+      boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);   // (*)
+      return InvokeProtectedService(plugin, service, parameters);
+    }
+  }
+
+
   bool OrthancPlugins::HasStorageArea() const
   {
+    boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
     return pimpl_->storageArea_.get() != NULL;
   }
   
   bool OrthancPlugins::HasDatabaseBackend() const
   {
+    boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
     return pimpl_->database_.get() != NULL;
   }
 
@@ -1898,4 +3333,210 @@
   {
     return pimpl_->dictionary_;
   }
+
+
+  IWorklistRequestHandler* OrthancPlugins::ConstructWorklistRequestHandler()
+  {
+    if (HasWorklistHandler())
+    {
+      return new WorklistHandler(*this);
+    }
+    else
+    {
+      return NULL;
+    }
+  }
+
+
+  bool OrthancPlugins::HasWorklistHandler()
+  {
+    boost::mutex::scoped_lock lock(pimpl_->worklistCallbackMutex_);
+    return pimpl_->worklistCallback_ != NULL;
+  }
+
+
+  IFindRequestHandler* OrthancPlugins::ConstructFindRequestHandler()
+  {
+    if (HasFindHandler())
+    {
+      return new FindHandler(*this);
+    }
+    else
+    {
+      return NULL;
+    }
+  }
+
+
+  bool OrthancPlugins::HasFindHandler()
+  {
+    boost::mutex::scoped_lock lock(pimpl_->findCallbackMutex_);
+    return pimpl_->findCallback_ != NULL;
+  }
+
+
+  IMoveRequestHandler* OrthancPlugins::ConstructMoveRequestHandler()
+  {
+    if (HasMoveHandler())
+    {
+      return new MoveHandler(*this);
+    }
+    else
+    {
+      return NULL;
+    }
+  }
+
+
+  bool OrthancPlugins::HasMoveHandler()
+  {
+    boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
+    return pimpl_->moveCallbacks_.callback != NULL;
+  }
+
+
+  bool OrthancPlugins::HasCustomImageDecoder()
+  {
+    boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_);
+    return !pimpl_->decodeImageCallbacks_.empty();
+  }
+
+
+  ImageAccessor*  OrthancPlugins::DecodeUnsafe(const void* dicom,
+                                               size_t size,
+                                               unsigned int frame)
+  {
+    boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_);
+
+    for (PImpl::DecodeImageCallbacks::const_iterator
+           decoder = pimpl_->decodeImageCallbacks_.begin();
+         decoder != pimpl_->decodeImageCallbacks_.end(); ++decoder)
+    {
+      OrthancPluginImage* pluginImage = NULL;
+      if ((*decoder) (&pluginImage, dicom, size, frame) == OrthancPluginErrorCode_Success &&
+          pluginImage != NULL)
+      {
+        return reinterpret_cast<ImageAccessor*>(pluginImage);
+      }
+    }
+
+    return NULL;
+  }
+
+
+  ImageAccessor* OrthancPlugins::Decode(const void* dicom,
+                                        size_t size,
+                                        unsigned int frame)
+  {
+    ImageAccessor* result = DecodeUnsafe(dicom, size, frame);
+
+    if (result != NULL)
+    {
+      return result;
+    }
+    else
+    {
+      LOG(INFO) << "The installed image decoding plugins cannot handle an image, fallback to the built-in decoder";
+      DefaultDicomImageDecoder defaultDecoder;
+      return defaultDecoder.Decode(dicom, size, frame); 
+    }
+  }
+
+  
+  bool OrthancPlugins::IsAllowed(HttpMethod method,
+                                 const char* uri,
+                                 const char* ip,
+                                 const char* username,
+                                 const IHttpHandler::Arguments& httpHeaders,
+                                 const IHttpHandler::GetArguments& getArguments)
+  {
+    OrthancPluginHttpMethod cMethod = Plugins::Convert(method);
+
+    std::vector<const char*> httpKeys(httpHeaders.size());
+    std::vector<const char*> httpValues(httpHeaders.size());
+
+    size_t pos = 0;
+    for (IHttpHandler::Arguments::const_iterator
+           it = httpHeaders.begin(); it != httpHeaders.end(); ++it, pos++)
+    {
+      httpKeys[pos] = it->first.c_str();
+      httpValues[pos] = it->second.c_str();
+    }
+
+    std::vector<const char*> getKeys(getArguments.size());
+    std::vector<const char*> getValues(getArguments.size());
+
+    for (size_t i = 0; i < getArguments.size(); i++)
+    {
+      getKeys[i] = getArguments[i].first.c_str();
+      getValues[i] = getArguments[i].second.c_str();
+    }
+
+    // Improved callback with support for GET arguments, since Orthanc 1.3.0
+    for (PImpl::IncomingHttpRequestFilters2::const_iterator
+           filter = pimpl_->incomingHttpRequestFilters2_.begin();
+         filter != pimpl_->incomingHttpRequestFilters2_.end(); ++filter)
+    {
+      int32_t allowed = (*filter) (cMethod, uri, ip,
+                                   httpKeys.size(),
+                                   httpKeys.empty() ? NULL : &httpKeys[0],
+                                   httpValues.empty() ? NULL : &httpValues[0],
+                                   getKeys.size(),
+                                   getKeys.empty() ? NULL : &getKeys[0],
+                                   getValues.empty() ? NULL : &getValues[0]);
+
+      if (allowed == 0)
+      {
+        return false;
+      }
+      else if (allowed != 1)
+      {
+        // The callback is only allowed to answer 0 or 1
+        throw OrthancException(ErrorCode_Plugin);
+      }
+    }
+
+    for (PImpl::IncomingHttpRequestFilters::const_iterator
+           filter = pimpl_->incomingHttpRequestFilters_.begin();
+         filter != pimpl_->incomingHttpRequestFilters_.end(); ++filter)
+    {
+      int32_t allowed = (*filter) (cMethod, uri, ip, httpKeys.size(),
+                                   httpKeys.empty() ? NULL : &httpKeys[0],
+                                   httpValues.empty() ? NULL : &httpValues[0]);
+
+      if (allowed == 0)
+      {
+        return false;
+      }
+      else if (allowed != 1)
+      {
+        // The callback is only allowed to answer 0 or 1
+        throw OrthancException(ErrorCode_Plugin);
+      }
+    }
+
+    return true;
+  }
+
+
+  IJob* OrthancPlugins::UnserializeJob(const std::string& type,
+                                       const Json::Value& value)
+  {
+    const std::string serialized = value.toStyledString();
+
+    boost::mutex::scoped_lock lock(pimpl_->jobsUnserializersMutex_);
+
+    for (PImpl::JobsUnserializers::iterator 
+           unserializer = pimpl_->jobsUnserializers_.begin();
+         unserializer != pimpl_->jobsUnserializers_.end(); ++unserializer)
+    {
+      OrthancPluginJob* job = (*unserializer) (type.c_str(), serialized.c_str());
+      if (job != NULL)
+      {
+        return reinterpret_cast<PluginsJob*>(job);
+      }
+    }
+
+    return NULL;
+  }
 }
--- a/Plugins/Engine/OrthancPlugins.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Engine/OrthancPlugins.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,7 +35,12 @@
 
 #include "PluginsErrorDictionary.h"
 
-#if ORTHANC_PLUGINS_ENABLED != 1
+#if !defined(ORTHANC_ENABLE_PLUGINS)
+#  error The macro ORTHANC_ENABLE_PLUGINS must be defined
+#endif
+
+
+#if ORTHANC_ENABLE_PLUGINS != 1
 
 #include <boost/noncopyable.hpp>
 
@@ -47,8 +53,14 @@
 
 #else
 
+#include "../../Core/DicomNetworking/IFindRequestHandlerFactory.h"
+#include "../../Core/DicomNetworking/IMoveRequestHandlerFactory.h"
+#include "../../Core/DicomNetworking/IWorklistRequestHandlerFactory.h"
 #include "../../Core/FileStorage/IStorageArea.h"
 #include "../../Core/HttpServer/IHttpHandler.h"
+#include "../../Core/HttpServer/IIncomingHttpRequestFilter.h"
+#include "../../Core/JobsEngine/IJob.h"
+#include "../../OrthancServer/IDicomImageDecoder.h"
 #include "../../OrthancServer/IServerListener.h"
 #include "OrthancPluginDatabase.h"
 #include "PluginsManager.h"
@@ -63,13 +75,20 @@
   class OrthancPlugins : 
     public IHttpHandler, 
     public IPluginServiceProvider, 
-    public IServerListener
+    public IServerListener,
+    public IWorklistRequestHandlerFactory,
+    public IDicomImageDecoder,
+    public IIncomingHttpRequestFilter,
+    public IFindRequestHandlerFactory,
+    public IMoveRequestHandlerFactory
   {
   private:
-    struct PImpl;
+    class PImpl;
     boost::shared_ptr<PImpl> pimpl_;
 
-    void CheckContextAvailable();
+    class WorklistHandler;
+    class FindHandler;
+    class MoveHandler;
 
     void RegisterRestCallback(const void* parameters,
                               bool lock);
@@ -78,6 +97,20 @@
 
     void RegisterOnChangeCallback(const void* parameters);
 
+    void RegisterWorklistCallback(const void* parameters);
+
+    void RegisterFindCallback(const void* parameters);
+
+    void RegisterMoveCallback(const void* parameters);
+
+    void RegisterDecodeImageCallback(const void* parameters);
+
+    void RegisterJobsUnserializer(const void* parameters);
+
+    void RegisterIncomingHttpRequestFilter(const void* parameters);
+
+    void RegisterIncomingHttpRequestFilter2(const void* parameters);
+
     void AnswerBuffer(const void* parameters);
 
     void Redirect(const void* parameters);
@@ -91,6 +124,8 @@
     void RestApiGet(const void* parameters,
                     bool afterPlugins);
 
+    void RestApiGet2(const void* parameters);
+
     void RestApiPostPut(bool isPost, 
                         const void* parameters,
                         bool afterPlugins);
@@ -123,17 +158,46 @@
 
     void CallHttpClient(const void* parameters);
 
+    void CallHttpClient2(const void* parameters);
+
+    void CallPeerApi(const void* parameters);
+  
     void GetFontInfo(const void* parameters);
 
     void DrawText(const void* parameters);
 
+    void DatabaseAnswer(const void* parameters);
+
     void ApplyDicomToJson(_OrthancPluginService service,
                           const void* parameters);
 
+    void ApplyCreateDicom(_OrthancPluginService service,
+                          const void* parameters);
+
+    void ApplyCreateImage(_OrthancPluginService service,
+                          const void* parameters);
+
+    void ApplyLookupDictionary(const void* parameters);
+
+    void ApplySendMultipartItem(const void* parameters);
+
+    void ApplySendMultipartItem2(const void* parameters);
+
+    void ComputeHash(_OrthancPluginService service,
+                     const void* parameters);
+
     void SignalChangeInternal(OrthancPluginChangeType changeType,
                               OrthancPluginResourceType resourceType,
                               const char* resource);
 
+    bool InvokeSafeService(SharedLibrary& plugin,
+                           _OrthancPluginService service,
+                           const void* parameters);
+
+    bool InvokeProtectedService(SharedLibrary& plugin,
+                                _OrthancPluginService service,
+                                const void* parameters);
+
   public:
     OrthancPlugins();
 
@@ -141,6 +205,8 @@
 
     void SetServerContext(ServerContext& context);
 
+    void ResetServerContext();
+
     virtual bool Handle(HttpOutput& output,
                         RequestOrigin origin,
                         const char* remoteIp,
@@ -200,6 +266,51 @@
     {
       SignalChangeInternal(OrthancPluginChangeType_OrthancStopped, OrthancPluginResourceType_None, NULL);
     }
+
+    void SignalUpdatedPeers()
+    {
+      SignalChangeInternal(OrthancPluginChangeType_UpdatedPeers, OrthancPluginResourceType_None, NULL);
+    }
+
+    void SignalUpdatedModalities()
+    {
+      SignalChangeInternal(OrthancPluginChangeType_UpdatedModalities, OrthancPluginResourceType_None, NULL);
+    }
+
+    bool HasWorklistHandler();
+
+    virtual IWorklistRequestHandler* ConstructWorklistRequestHandler();
+
+    bool HasCustomImageDecoder();
+
+    // Contrarily to "Decode()", this method does not fallback to the
+    // builtin image decoder, if no installed custom decoder can
+    // handle the image (it returns NULL in this case).
+    ImageAccessor* DecodeUnsafe(const void* dicom,
+                                size_t size,
+                                unsigned int frame);
+
+    virtual ImageAccessor* Decode(const void* dicom,
+                                  size_t size,
+                                  unsigned int frame);
+
+    virtual bool IsAllowed(HttpMethod method,
+                           const char* uri,
+                           const char* ip,
+                           const char* username,
+                           const IHttpHandler::Arguments& httpHeaders,
+                           const IHttpHandler::GetArguments& getArguments);
+
+    bool HasFindHandler();
+
+    virtual IFindRequestHandler* ConstructFindRequestHandler();
+
+    bool HasMoveHandler();
+
+    virtual IMoveRequestHandler* ConstructMoveRequestHandler();
+
+    IJob* UnserializeJob(const std::string& type,
+                         const Json::Value& value);
   };
 }
 
--- a/Plugins/Engine/PluginsEnumerations.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Engine/PluginsEnumerations.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,7 +34,7 @@
 #include "../../OrthancServer/PrecompiledHeadersServer.h"
 #include "PluginsEnumerations.h"
 
-#if ORTHANC_PLUGINS_ENABLED != 1
+#if ORTHANC_ENABLE_PLUGINS != 1
 #error The plugin support is disabled
 #endif
 
@@ -122,6 +123,12 @@
         case ChangeType_StableStudy:
           return OrthancPluginChangeType_StableStudy;
 
+        case ChangeType_UpdatedAttachment:
+          return OrthancPluginChangeType_UpdatedAttachment;
+
+        case ChangeType_UpdatedMetadata:
+          return OrthancPluginChangeType_UpdatedMetadata;
+
         default:
           throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
@@ -132,15 +139,30 @@
     {
       switch (format)
       {
+        case PixelFormat_BGRA32:
+          return OrthancPluginPixelFormat_BGRA32;
+
+        case PixelFormat_Float32:
+          return OrthancPluginPixelFormat_Float32;
+
         case PixelFormat_Grayscale16:
           return OrthancPluginPixelFormat_Grayscale16;
 
+        case PixelFormat_Grayscale32:
+          return OrthancPluginPixelFormat_Grayscale32;
+
+        case PixelFormat_Grayscale64:
+          return OrthancPluginPixelFormat_Grayscale64;
+
         case PixelFormat_Grayscale8:
           return OrthancPluginPixelFormat_Grayscale8;
 
         case PixelFormat_RGB24:
           return OrthancPluginPixelFormat_RGB24;
 
+        case PixelFormat_RGB48:
+          return OrthancPluginPixelFormat_RGB48;
+
         case PixelFormat_RGBA32:
           return OrthancPluginPixelFormat_RGBA32;
 
@@ -157,15 +179,30 @@
     {
       switch (format)
       {
+        case OrthancPluginPixelFormat_BGRA32:
+          return PixelFormat_BGRA32;
+
+        case OrthancPluginPixelFormat_Float32:
+          return PixelFormat_Float32;
+
         case OrthancPluginPixelFormat_Grayscale16:
           return PixelFormat_Grayscale16;
 
+        case OrthancPluginPixelFormat_Grayscale32:
+          return PixelFormat_Grayscale32;
+
+        case OrthancPluginPixelFormat_Grayscale64:
+          return PixelFormat_Grayscale64;
+
         case OrthancPluginPixelFormat_Grayscale8:
           return PixelFormat_Grayscale8;
 
         case OrthancPluginPixelFormat_RGB24:
           return PixelFormat_RGB24;
 
+        case OrthancPluginPixelFormat_RGB48:
+          return PixelFormat_RGB48;
+
         case OrthancPluginPixelFormat_RGBA32:
           return PixelFormat_RGBA32;
 
@@ -220,8 +257,8 @@
         case OrthancPluginDicomToJsonFormat_Short:
           return DicomToJsonFormat_Short;
 
-        case OrthancPluginDicomToJsonFormat_Simple:
-          return DicomToJsonFormat_Simple;
+        case OrthancPluginDicomToJsonFormat_Human:
+          return DicomToJsonFormat_Human;
 
         default:
           throw OrthancException(ErrorCode_ParameterOutOfRange);
@@ -273,96 +310,285 @@
     }
 
 
-#if !defined(ORTHANC_ENABLE_DCMTK) || ORTHANC_ENABLE_DCMTK != 0
-    DcmEVR Convert(OrthancPluginValueRepresentation vr)
+    OrthancPluginInstanceOrigin Convert(RequestOrigin origin)
     {
-      switch (vr)
+      switch (origin)
       {
-        case OrthancPluginValueRepresentation_AE:
-          return EVR_AE;
-
-        case OrthancPluginValueRepresentation_AS:
-          return EVR_AS;
-
-        case OrthancPluginValueRepresentation_AT:
-          return EVR_AT;
-
-        case OrthancPluginValueRepresentation_CS:
-          return EVR_CS;
-
-        case OrthancPluginValueRepresentation_DA:
-          return EVR_DA;
+        case RequestOrigin_DicomProtocol:
+          return OrthancPluginInstanceOrigin_DicomProtocol;
 
-        case OrthancPluginValueRepresentation_DS:
-          return EVR_DS;
+        case RequestOrigin_RestApi:
+          return OrthancPluginInstanceOrigin_RestApi;
 
-        case OrthancPluginValueRepresentation_DT:
-          return EVR_DT;
-
-        case OrthancPluginValueRepresentation_FD:
-          return EVR_FD;
+        case RequestOrigin_Lua:
+          return OrthancPluginInstanceOrigin_Lua;
 
-        case OrthancPluginValueRepresentation_FL:
-          return EVR_FL;
-
-        case OrthancPluginValueRepresentation_IS:
-          return EVR_IS;
+        case RequestOrigin_Plugins:
+          return OrthancPluginInstanceOrigin_Plugin;
 
-        case OrthancPluginValueRepresentation_LO:
-          return EVR_LO;
-
-        case OrthancPluginValueRepresentation_LT:
-          return EVR_LT;
+        case RequestOrigin_Unknown:
+          return OrthancPluginInstanceOrigin_Unknown;
 
-        case OrthancPluginValueRepresentation_OB:
-          return EVR_OB;
-
-        case OrthancPluginValueRepresentation_OF:
-          return EVR_OF;
-
-        case OrthancPluginValueRepresentation_OW:
-          return EVR_OW;
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
 
-        case OrthancPluginValueRepresentation_PN:
-          return EVR_PN;
-
-        case OrthancPluginValueRepresentation_SH:
-          return EVR_SH;
-
-        case OrthancPluginValueRepresentation_SL:
-          return EVR_SL;
-
-        case OrthancPluginValueRepresentation_SQ:
-          return EVR_SQ;
 
-        case OrthancPluginValueRepresentation_SS:
-          return EVR_SS;
-
-        case OrthancPluginValueRepresentation_ST:
-          return EVR_ST;
-
-        case OrthancPluginValueRepresentation_TM:
-          return EVR_TM;
-
-        case OrthancPluginValueRepresentation_UI:
-          return EVR_UI;
+    OrthancPluginHttpMethod Convert(HttpMethod method)
+    {
+      switch (method)
+      {
+        case HttpMethod_Get:
+          return OrthancPluginHttpMethod_Get;
 
-        case OrthancPluginValueRepresentation_UL:
-          return EVR_UL;
-
-        case OrthancPluginValueRepresentation_UN:
-          return EVR_UN;
+        case HttpMethod_Post:
+          return OrthancPluginHttpMethod_Post;
 
-        case OrthancPluginValueRepresentation_US:
-          return EVR_US;
+        case HttpMethod_Put:
+          return OrthancPluginHttpMethod_Put;
 
-        case OrthancPluginValueRepresentation_UT:
-          return EVR_UT;
+        case HttpMethod_Delete:
+          return OrthancPluginHttpMethod_Delete;
 
         default:
           throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
     }
-#endif
+
+
+    ValueRepresentation Convert(OrthancPluginValueRepresentation vr)
+    {
+      switch (vr)
+      {
+        case OrthancPluginValueRepresentation_AE:
+          return ValueRepresentation_ApplicationEntity;
+
+        case OrthancPluginValueRepresentation_AS:
+          return ValueRepresentation_AgeString;
+
+        case OrthancPluginValueRepresentation_AT:
+          return ValueRepresentation_AttributeTag;
+
+        case OrthancPluginValueRepresentation_CS:
+          return ValueRepresentation_CodeString;
+
+        case OrthancPluginValueRepresentation_DA:
+          return ValueRepresentation_Date;
+
+        case OrthancPluginValueRepresentation_DS:
+          return ValueRepresentation_DecimalString;
+
+        case OrthancPluginValueRepresentation_DT:
+          return ValueRepresentation_DateTime;
+
+        case OrthancPluginValueRepresentation_FD:
+          return ValueRepresentation_FloatingPointDouble;
+
+        case OrthancPluginValueRepresentation_FL:
+          return ValueRepresentation_FloatingPointSingle;
+
+        case OrthancPluginValueRepresentation_IS:
+          return ValueRepresentation_IntegerString;
+
+        case OrthancPluginValueRepresentation_LO:
+          return ValueRepresentation_LongString;
+
+        case OrthancPluginValueRepresentation_LT:
+          return ValueRepresentation_LongText;
+
+        case OrthancPluginValueRepresentation_OB:
+          return ValueRepresentation_OtherByte;
+
+        case OrthancPluginValueRepresentation_OF:
+          return ValueRepresentation_OtherFloat;
+
+        case OrthancPluginValueRepresentation_OW:
+          return ValueRepresentation_OtherWord;
+
+        case OrthancPluginValueRepresentation_PN:
+          return ValueRepresentation_PersonName;
+
+        case OrthancPluginValueRepresentation_SH:
+          return ValueRepresentation_ShortString;
+
+        case OrthancPluginValueRepresentation_SL:
+          return ValueRepresentation_SignedLong;
+
+        case OrthancPluginValueRepresentation_SQ:
+          return ValueRepresentation_Sequence;
+
+        case OrthancPluginValueRepresentation_SS:
+          return ValueRepresentation_SignedShort;
+
+        case OrthancPluginValueRepresentation_ST:
+          return ValueRepresentation_ShortText;
+
+        case OrthancPluginValueRepresentation_TM:
+          return ValueRepresentation_Time;
+
+        case OrthancPluginValueRepresentation_UI:
+          return ValueRepresentation_UniqueIdentifier;
+
+        case OrthancPluginValueRepresentation_UL:
+          return ValueRepresentation_UnsignedLong;
+
+        case OrthancPluginValueRepresentation_UN:
+          return ValueRepresentation_Unknown;
+
+        case OrthancPluginValueRepresentation_US:
+          return ValueRepresentation_UnsignedShort;
+
+        case OrthancPluginValueRepresentation_UT:
+          return ValueRepresentation_UnlimitedText;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+
+          /*
+          Not supported as of DCMTK 3.6.0:
+          return ValueRepresentation_OtherDouble
+          return ValueRepresentation_OtherLong
+          return ValueRepresentation_UniversalResource
+          return ValueRepresentation_UnlimitedCharacters
+          */
+      }
+    }
+
+
+    OrthancPluginValueRepresentation Convert(ValueRepresentation vr)
+    {
+      switch (vr)
+      {
+        case ValueRepresentation_ApplicationEntity:
+          return OrthancPluginValueRepresentation_AE;
+
+        case ValueRepresentation_AgeString:
+          return OrthancPluginValueRepresentation_AS;
+
+        case ValueRepresentation_AttributeTag:
+          return OrthancPluginValueRepresentation_AT;
+
+        case ValueRepresentation_CodeString:
+          return OrthancPluginValueRepresentation_CS;
+
+        case ValueRepresentation_Date:
+          return OrthancPluginValueRepresentation_DA;
+
+        case ValueRepresentation_DecimalString:
+          return OrthancPluginValueRepresentation_DS;
+
+        case ValueRepresentation_DateTime:
+          return OrthancPluginValueRepresentation_DT;
+
+        case ValueRepresentation_FloatingPointDouble:
+          return OrthancPluginValueRepresentation_FD;
+
+        case ValueRepresentation_FloatingPointSingle:
+          return OrthancPluginValueRepresentation_FL;
+
+        case ValueRepresentation_IntegerString:
+          return OrthancPluginValueRepresentation_IS;
+
+        case ValueRepresentation_LongString:
+          return OrthancPluginValueRepresentation_LO;
+
+        case ValueRepresentation_LongText:
+          return OrthancPluginValueRepresentation_LT;
+
+        case ValueRepresentation_OtherByte:
+          return OrthancPluginValueRepresentation_OB;
+
+        case ValueRepresentation_OtherFloat:
+          return OrthancPluginValueRepresentation_OF;
+
+        case ValueRepresentation_OtherWord:
+          return OrthancPluginValueRepresentation_OW;
+
+        case ValueRepresentation_PersonName:
+          return OrthancPluginValueRepresentation_PN;
+
+        case ValueRepresentation_ShortString:
+          return OrthancPluginValueRepresentation_SH;
+
+        case ValueRepresentation_SignedLong:
+          return OrthancPluginValueRepresentation_SL;
+
+        case ValueRepresentation_Sequence:
+          return OrthancPluginValueRepresentation_SQ;
+
+        case ValueRepresentation_SignedShort:
+          return OrthancPluginValueRepresentation_SS;
+
+        case ValueRepresentation_ShortText:
+          return OrthancPluginValueRepresentation_ST;
+
+        case ValueRepresentation_Time:
+          return OrthancPluginValueRepresentation_TM;
+
+        case ValueRepresentation_UniqueIdentifier:
+          return OrthancPluginValueRepresentation_UI;
+
+        case ValueRepresentation_UnsignedLong:
+          return OrthancPluginValueRepresentation_UL;
+
+        case ValueRepresentation_UnsignedShort:
+          return OrthancPluginValueRepresentation_US;
+
+        case ValueRepresentation_UnlimitedText:
+          return OrthancPluginValueRepresentation_UT;
+
+        case ValueRepresentation_Unknown:
+          return OrthancPluginValueRepresentation_UN;  // Unknown
+
+          // These VR are not supported as of DCMTK 3.6.0, so they are
+          // mapped to "UN" (unknown) VR in the plugins
+        case ValueRepresentation_OtherDouble:          
+        case ValueRepresentation_OtherLong:
+        case ValueRepresentation_UniversalResource:
+        case ValueRepresentation_UnlimitedCharacters:
+          return OrthancPluginValueRepresentation_UN;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    OrthancPluginJobStepStatus Convert(JobStepCode step)
+    {
+      switch (step)
+      {
+        case JobStepCode_Success:
+          return OrthancPluginJobStepStatus_Success;
+          
+        case JobStepCode_Failure:
+          return OrthancPluginJobStepStatus_Failure;
+          
+        case JobStepCode_Continue:
+          return OrthancPluginJobStepStatus_Continue;
+        
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    JobStepCode Convert(OrthancPluginJobStepStatus step)
+    {
+      switch (step)
+      {
+        case OrthancPluginJobStepStatus_Success:
+          return JobStepCode_Success;
+        
+        case OrthancPluginJobStepStatus_Failure:
+          return JobStepCode_Failure;
+        
+        case OrthancPluginJobStepStatus_Continue:
+          return JobStepCode_Continue;
+        
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
   }
 }
--- a/Plugins/Engine/PluginsEnumerations.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Engine/PluginsEnumerations.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,15 +33,11 @@
 
 #pragma once
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
 
 #include "../Include/orthanc/OrthancCPlugin.h"
 #include "../../OrthancServer/ServerEnumerations.h"
 
-#if !defined(ORTHANC_ENABLE_DCMTK) || ORTHANC_ENABLE_DCMTK != 0
-#include <dcmtk/dcmdata/dcvr.h>
-#endif
-
 namespace Orthanc
 {
   namespace Plugins
@@ -65,9 +62,17 @@
 
     IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint);
 
-#if !defined(ORTHANC_ENABLE_DCMTK) || ORTHANC_ENABLE_DCMTK != 0
-    DcmEVR Convert(OrthancPluginValueRepresentation vr);
-#endif
+    OrthancPluginInstanceOrigin Convert(RequestOrigin origin);
+
+    OrthancPluginHttpMethod Convert(HttpMethod method);
+
+    ValueRepresentation Convert(OrthancPluginValueRepresentation vr);
+
+    OrthancPluginValueRepresentation Convert(ValueRepresentation vr);
+
+    OrthancPluginJobStepStatus Convert(JobStepCode step);
+
+    JobStepCode Convert(OrthancPluginJobStepStatus step);
   }
 }
 
--- a/Plugins/Engine/PluginsErrorDictionary.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Engine/PluginsErrorDictionary.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,7 +34,7 @@
 #include "../../OrthancServer/PrecompiledHeadersServer.h"
 #include "PluginsErrorDictionary.h"
 
-#if ORTHANC_PLUGINS_ENABLED != 1
+#if ORTHANC_ENABLE_PLUGINS != 1
 #error The plugin support is disabled
 #endif
 
--- a/Plugins/Engine/PluginsErrorDictionary.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Engine/PluginsErrorDictionary.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,11 +33,11 @@
 
 #pragma once
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
 
 #include "../Include/orthanc/OrthancCPlugin.h"
 #include "../../Core/OrthancException.h"
-#include "SharedLibrary.h"
+#include "../../Core/SharedLibrary.h"
 
 #include <map>
 #include <string>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Engine/PluginsJob.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,189 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../OrthancServer/PrecompiledHeadersServer.h"
+#include "PluginsJob.h"
+
+#if ORTHANC_ENABLE_PLUGINS != 1
+#error The plugin support is disabled
+#endif
+
+
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+
+#include <json/reader.h>
+#include <cassert>
+
+namespace Orthanc
+{
+  PluginsJob::PluginsJob(const _OrthancPluginCreateJob& parameters) :
+    parameters_(parameters)
+  {
+    if (parameters_.job == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    
+    if (parameters_.target == NULL ||
+        parameters_.finalize == NULL ||
+        parameters_.type == NULL ||
+        parameters_.getProgress == NULL ||
+        parameters_.getContent == NULL ||
+        parameters_.getSerialized == NULL ||
+        parameters_.step == NULL ||
+        parameters_.stop == NULL ||
+        parameters_.reset == NULL)
+    {
+      parameters_.finalize(parameters.job);
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    type_.assign(parameters.type);
+  }
+
+  PluginsJob::~PluginsJob()
+  {
+    assert(parameters_.job != NULL);
+    parameters_.finalize(parameters_.job);
+  }
+
+  JobStepResult PluginsJob::Step()
+  {
+    OrthancPluginJobStepStatus status = parameters_.step(parameters_.job);
+
+    switch (status)
+    {
+      case OrthancPluginJobStepStatus_Success:
+        return JobStepResult::Success();
+
+      case OrthancPluginJobStepStatus_Failure:
+        return JobStepResult::Failure(ErrorCode_Plugin);
+
+      case OrthancPluginJobStepStatus_Continue:
+        return JobStepResult::Continue();
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  void PluginsJob::Reset()
+  {
+    parameters_.reset(parameters_.job);
+  }
+
+  void PluginsJob::Stop(JobStopReason reason)
+  {
+    switch (reason)
+    {
+      case JobStopReason_Success:
+        parameters_.stop(parameters_.job, OrthancPluginJobStopReason_Success);
+        break;
+
+      case JobStopReason_Failure:
+        parameters_.stop(parameters_.job, OrthancPluginJobStopReason_Failure);
+        break;
+
+      case JobStopReason_Canceled:
+        parameters_.stop(parameters_.job, OrthancPluginJobStopReason_Canceled);
+        break;
+
+      case JobStopReason_Paused:
+        parameters_.stop(parameters_.job, OrthancPluginJobStopReason_Paused);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  float PluginsJob::GetProgress()
+  {
+    return parameters_.getProgress(parameters_.job);
+  }
+
+  void PluginsJob::GetPublicContent(Json::Value& value)
+  {
+    const char* content = parameters_.getContent(parameters_.job);
+
+    if (content == NULL)
+    {
+      value = Json::objectValue;
+    }
+    else
+    {
+      Json::Reader reader;
+      
+      if (!reader.parse(content, value) ||
+          value.type() != Json::objectValue)
+      {
+        LOG(ERROR) << "A job plugin must provide a JSON object as its public content";
+        throw OrthancException(ErrorCode_Plugin);
+      }
+    }
+  }
+
+  bool PluginsJob::Serialize(Json::Value& value)
+  {
+    const char* serialized = parameters_.getSerialized(parameters_.job);
+
+    if (serialized == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      Json::Reader reader;
+      
+      if (!reader.parse(serialized, value) ||
+          value.type() != Json::objectValue)
+      {
+        LOG(ERROR) << "A job plugin must provide a JSON object as its serialized content";
+        throw OrthancException(ErrorCode_Plugin);
+      }
+
+
+      static const char* KEY_TYPE = "Type";
+      
+      if (value.isMember(KEY_TYPE))
+      {
+        LOG(ERROR) << "The \"Type\" field is for reserved use for serialized job";
+        throw OrthancException(ErrorCode_Plugin);
+      }
+
+      value[KEY_TYPE] = type_;
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Engine/PluginsJob.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+
+#include "../../Core/JobsEngine/IJob.h"
+#include "../Include/orthanc/OrthancCPlugin.h"
+
+namespace Orthanc
+{
+  class PluginsJob : public IJob
+  {
+  private:
+    _OrthancPluginCreateJob  parameters_;
+    std::string              type_;
+
+  public:
+    PluginsJob(const _OrthancPluginCreateJob& parameters);
+
+    virtual ~PluginsJob();
+
+    virtual void Start()
+    {
+    }
+    
+    virtual JobStepResult Step();
+
+    virtual void Reset();
+
+    virtual void Stop(JobStopReason reason);
+
+    virtual float GetProgress();
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = type_;
+    }
+    
+    virtual void GetPublicContent(Json::Value& value);
+
+    virtual bool Serialize(Json::Value& value);
+  };
+}
+
+#endif
--- a/Plugins/Engine/PluginsManager.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Engine/PluginsManager.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,14 +34,14 @@
 #include "../../OrthancServer/PrecompiledHeadersServer.h"
 #include "PluginsManager.h"
 
-#if ORTHANC_PLUGINS_ENABLED != 1
+#if ORTHANC_ENABLE_PLUGINS != 1
 #error The plugin support is disabled
 #endif
 
-
-#include "../../Core/Toolbox.h"
 #include "../../Core/HttpServer/HttpOutput.h"
 #include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/Toolbox.h"
 
 #include <cassert>
 #include <memory>
@@ -48,7 +49,7 @@
 
 #ifdef WIN32
 #define PLUGIN_EXTENSION ".dll"
-#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
+#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__)
 #define PLUGIN_EXTENSION ".so"
 #elif defined(__APPLE__) && defined(__MACH__)
 #define PLUGIN_EXTENSION ".dylib"
@@ -191,7 +192,11 @@
       catch (OrthancException& e)
       {
         // This service provider has failed
-        LOG(ERROR) << "Exception while invoking plugin service " << service << ": " << e.What();
+        if (e.GetErrorCode() != ErrorCode_UnknownResource)  // This error code is valid in plugins
+        {
+          LOG(ERROR) << "Exception while invoking plugin service " << service << ": " << e.What();
+        }
+
         return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
       }
     }
--- a/Plugins/Engine/PluginsManager.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Engine/PluginsManager.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,9 +33,8 @@
 
 #pragma once
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
 
-#include "SharedLibrary.h"
 #include "IPluginServiceProvider.h"
 
 #include <map>
--- a/Plugins/Engine/SharedLibrary.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,140 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../../OrthancServer/PrecompiledHeadersServer.h"
-#include "SharedLibrary.h"
-
-#if ORTHANC_PLUGINS_ENABLED != 1
-#error The plugin support is disabled
-#endif
-
-
-#include "../../Core/Logging.h"
-#include "../../Core/Toolbox.h"
-
-#include <boost/filesystem.hpp>
-
-#if defined(_WIN32)
-#include <windows.h>
-#elif defined(__linux) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
-#include <dlfcn.h>
-#else
-#error Support your platform here
-#endif
-
-namespace Orthanc
-{
-  SharedLibrary::SharedLibrary(const std::string& path) : 
-    path_(path), 
-    handle_(NULL)
-  {
-#if defined(_WIN32)
-    handle_ = ::LoadLibraryA(path_.c_str());
-    if (handle_ == NULL)
-    {
-      LOG(ERROR) << "LoadLibrary(" << path_ << ") failed: Error " << ::GetLastError();
-      throw OrthancException(ErrorCode_SharedLibrary);
-    }
-
-#elif defined(__linux) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
-    handle_ = ::dlopen(path_.c_str(), RTLD_NOW);
-    if (handle_ == NULL) 
-    {
-      std::string explanation;
-      const char *tmp = ::dlerror();
-      if (tmp)
-      {
-        explanation = ": Error " + std::string(tmp);
-      }
-
-      LOG(ERROR) << "dlopen(" << path_ << ") failed" << explanation;
-      throw OrthancException(ErrorCode_SharedLibrary);
-    }
-
-#else
-#error Support your platform here
-#endif   
-  }
-
-  SharedLibrary::~SharedLibrary()
-  {
-    if (handle_)
-    {
-#if defined(_WIN32)
-      ::FreeLibrary((HMODULE)handle_);
-#elif defined(__linux) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
-      ::dlclose(handle_);
-#else
-#error Support your platform here
-#endif
-    }
-  }
-
-
-  SharedLibrary::FunctionPointer SharedLibrary::GetFunctionInternal(const std::string& name)
-  {
-    if (!handle_)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-#if defined(_WIN32)
-    return ::GetProcAddress((HMODULE)handle_, name.c_str());
-#elif defined(__linux) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
-    return ::dlsym(handle_, name.c_str());
-#else
-#error Support your platform here
-#endif
-  }
-
-
-  SharedLibrary::FunctionPointer SharedLibrary::GetFunction(const std::string& name)
-  {
-    SharedLibrary::FunctionPointer result = GetFunctionInternal(name);
-  
-    if (result == NULL)
-    {
-      LOG(ERROR) << "Shared library does not expose function \"" << name << "\"";
-      throw OrthancException(ErrorCode_SharedLibrary);
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  bool SharedLibrary::HasFunction(const std::string& name)
-  {
-    return GetFunctionInternal(name) != NULL;
-  }
-}
--- a/Plugins/Engine/SharedLibrary.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#if ORTHANC_PLUGINS_ENABLED == 1
-
-#include "../../Core/OrthancException.h"
-
-#include <boost/noncopyable.hpp>
-
-#if defined(_WIN32)
-#include <windows.h>
-#endif
-
-namespace Orthanc
-{
-  class SharedLibrary : public boost::noncopyable
-  {
-  public:
-#if defined(_WIN32)
-    typedef FARPROC FunctionPointer;
-#else
-    typedef void* FunctionPointer;
-#endif
-
-  private:
-    std::string path_;
-    void *handle_;
-
-    FunctionPointer GetFunctionInternal(const std::string& name);
-
-  public:
-    SharedLibrary(const std::string& path);
-
-    ~SharedLibrary();
-
-    const std::string& GetPath() const
-    {
-      return path_;
-    }
-
-    bool HasFunction(const std::string& name);
-
-    FunctionPointer GetFunction(const std::string& name);
-  };
-}
-
-#endif
--- a/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Fri Oct 12 15:18:10 2018 +0200
@@ -4,8 +4,9 @@
 
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -681,6 +682,18 @@
       OrthancPluginResourceType resourceType,
       const OrthancPluginDicomTag* tag,
       OrthancPluginIdentifierConstraint constraint);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    OrthancPluginErrorCode  (*lookupIdentifierRange) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      OrthancPluginResourceType resourceType,
+      uint16_t group,
+      uint16_t element,
+      const char* start,
+      const char* end);
    } OrthancPluginDatabaseExtensions;
 
 /*<! @endcond */
@@ -694,13 +707,8 @@
   } _OrthancPluginRegisterDatabaseBackend;
 
   /**
-   * Register a custom database back-end.
+   * Register a custom database back-end (for legacy plugins).
    *
-   * Instead of manually filling the OrthancPluginDatabaseBackend
-   * structure, you should instead implement a concrete C++ class
-   * deriving from ::OrthancPlugins::IDatabaseBackend, and register it
-   * using ::OrthancPlugins::DatabaseBackendAdapter::Register().
-   * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param backend The callbacks of the custom database engine.
    * @param payload Pointer containing private information for the database engine.
@@ -753,11 +761,6 @@
   /**
    * Register a custom database back-end.
    *
-   * Instead of manually filling the OrthancPluginDatabaseBackendV2
-   * structure, you should instead implement a concrete C++ class
-   * deriving from ::OrthancPlugins::IDatabaseBackend, and register it
-   * using ::OrthancPlugins::DatabaseBackendAdapter::Register().
-   * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param backend The callbacks of the custom database engine.
    * @param payload Pointer containing private information for the database engine.
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Oct 12 15:18:10 2018 +0200
@@ -18,6 +18,12 @@
  *    - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback().
  *    - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea().
  *    - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV2().
+ *    - Possibly register a handler for C-Find SCP using OrthancPluginRegisterFindCallback().
+ *    - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback().
+ *    - Possibly register a handler for C-Move SCP using OrthancPluginRegisterMoveCallback().
+ *    - Possibly register a custom decoder for DICOM images using OrthancPluginRegisterDecodeImageCallback().
+ *    - Possibly register a callback to filter incoming HTTP requests using OrthancPluginRegisterIncomingHttpRequestFilter2().
+ *    - Possibly register a callback to unserialize jobs using OrthancPluginRegisterJobsUnserializer().
  * -# <tt>void OrthancPluginFinalize()</tt>:
  *    This function is invoked by Orthanc during its shutdown. The plugin
  *    must free all its memory.
@@ -49,6 +55,9 @@
  * @defgroup Callbacks Callbacks
  * @brief Functions to register and manage callbacks by the plugins.
  *
+ * @defgroup DicomCallbacks DicomCallbacks
+ * @brief Functions to register and manage DICOM callbacks (worklists, C-Find, C-MOVE).
+ *
  * @defgroup Orthanc Orthanc
  * @brief Functions to access the content of the Orthanc server.
  **/
@@ -64,8 +73,9 @@
 
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -107,9 +117,19 @@
 #define ORTHANC_PLUGINS_API
 #endif
 
-#define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     0
-#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     9
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  5
+#define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
+#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     4
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  2
+
+
+#if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
+#define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision) \
+  (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major ||               \
+   (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major &&             \
+    (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor ||             \
+     (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor &&           \
+      ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision))))
+#endif
 
 
 
@@ -183,7 +203,7 @@
     OrthancPluginErrorCode_Plugin = 1    /*!< Error encountered within the plugin engine */,
     OrthancPluginErrorCode_NotImplemented = 2    /*!< Not implemented yet */,
     OrthancPluginErrorCode_ParameterOutOfRange = 3    /*!< Parameter out of range */,
-    OrthancPluginErrorCode_NotEnoughMemory = 4    /*!< Not enough memory */,
+    OrthancPluginErrorCode_NotEnoughMemory = 4    /*!< The server hosting Orthanc is running out of memory */,
     OrthancPluginErrorCode_BadParameterType = 5    /*!< Bad type for a parameter */,
     OrthancPluginErrorCode_BadSequenceOfCalls = 6    /*!< Bad sequence of calls */,
     OrthancPluginErrorCode_InexistentItem = 7    /*!< Accessing an inexistent item */,
@@ -213,6 +233,10 @@
     OrthancPluginErrorCode_DatabasePlugin = 31    /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */,
     OrthancPluginErrorCode_StorageAreaPlugin = 32    /*!< Error in the plugin implementing a custom storage area */,
     OrthancPluginErrorCode_EmptyRequest = 33    /*!< The request is empty */,
+    OrthancPluginErrorCode_NotAcceptable = 34    /*!< Cannot send a response which is acceptable according to the Accept HTTP header */,
+    OrthancPluginErrorCode_NullPointer = 35    /*!< Cannot handle a NULL pointer */,
+    OrthancPluginErrorCode_DatabaseUnavailable = 36    /*!< The database is currently not available (probably a transient situation) */,
+    OrthancPluginErrorCode_CanceledJob = 37    /*!< This job was canceled */,
     OrthancPluginErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     OrthancPluginErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
@@ -232,8 +256,8 @@
     OrthancPluginErrorCode_DirectoryOverFile = 2000    /*!< The directory to be created is already occupied by a regular file */,
     OrthancPluginErrorCode_FileStorageCannotWrite = 2001    /*!< Unable to create a subdirectory or a file in the file storage */,
     OrthancPluginErrorCode_DirectoryExpected = 2002    /*!< The specified path does not point to a directory */,
-    OrthancPluginErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is already in use */,
-    OrthancPluginErrorCode_DicomPortInUse = 2004    /*!< The TCP port of the DICOM server is already in use */,
+    OrthancPluginErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is privileged or already in use */,
+    OrthancPluginErrorCode_DicomPortInUse = 2004    /*!< The TCP port of the DICOM server is privileged or already in use */,
     OrthancPluginErrorCode_BadHttpStatusInRest = 2005    /*!< This HTTP status is not allowed in a REST API */,
     OrthancPluginErrorCode_RegularFileExpected = 2006    /*!< The specified path does not point to a regular file */,
     OrthancPluginErrorCode_PathToExecutable = 2007    /*!< Unable to get the path to the executable */,
@@ -270,6 +294,8 @@
     OrthancPluginErrorCode_DatabaseNotInitialized = 2038    /*!< Plugin trying to call the database during its initialization */,
     OrthancPluginErrorCode_SslDisabled = 2039    /*!< Orthanc has been built without SSL support */,
     OrthancPluginErrorCode_CannotOrderSlices = 2040    /*!< Unable to order the slices of the series */,
+    OrthancPluginErrorCode_NoWorklistHandler = 2041    /*!< No request handler factory for DICOM C-Find Modality SCP */,
+    OrthancPluginErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
 
     _OrthancPluginErrorCode_INTERNAL = 0x7fffffff
   } OrthancPluginErrorCode;
@@ -390,6 +416,13 @@
     _OrthancPluginService_RegisterDictionaryTag = 20,
     _OrthancPluginService_DicomBufferToJson = 21,
     _OrthancPluginService_DicomInstanceToJson = 22,
+    _OrthancPluginService_CreateDicom = 23,
+    _OrthancPluginService_ComputeMd5 = 24,
+    _OrthancPluginService_ComputeSha1 = 25,
+    _OrthancPluginService_LookupDictionary = 26,
+    _OrthancPluginService_CallHttpClient2 = 27,
+    _OrthancPluginService_GenerateUuid = 28,
+    _OrthancPluginService_RegisterPrivateDictionaryTag = 29,
 
     /* Registration of callbacks */
     _OrthancPluginService_RegisterRestCallback = 1000,
@@ -397,6 +430,12 @@
     _OrthancPluginService_RegisterStorageArea = 1002,
     _OrthancPluginService_RegisterOnChangeCallback = 1003,
     _OrthancPluginService_RegisterRestCallbackNoLock = 1004,
+    _OrthancPluginService_RegisterWorklistCallback = 1005,
+    _OrthancPluginService_RegisterDecodeImageCallback = 1006,
+    _OrthancPluginService_RegisterIncomingHttpRequestFilter = 1007,
+    _OrthancPluginService_RegisterFindCallback = 1008,
+    _OrthancPluginService_RegisterMoveCallback = 1009,
+    _OrthancPluginService_RegisterIncomingHttpRequestFilter2 = 1010,
 
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
@@ -411,6 +450,7 @@
     _OrthancPluginService_SendMultipartItem = 2009,
     _OrthancPluginService_SendHttpStatus = 2010,
     _OrthancPluginService_CompressAndAnswerImage = 2011,
+    _OrthancPluginService_SendMultipartItem2 = 2012,
 
     /* Access to the Orthanc database and API */
     _OrthancPluginService_GetDicomForInstance = 3000,
@@ -428,6 +468,7 @@
     _OrthancPluginService_RestApiDeleteAfterPlugins = 3012,
     _OrthancPluginService_RestApiPutAfterPlugins = 3013,
     _OrthancPluginService_ReconstructMainDicomTags = 3014,
+    _OrthancPluginService_RestApiGet2 = 3015,
 
     /* Access to DICOM instances */
     _OrthancPluginService_GetInstanceRemoteAet = 4000,
@@ -437,6 +478,7 @@
     _OrthancPluginService_GetInstanceSimplifiedJson = 4004,
     _OrthancPluginService_HasInstanceMetadata = 4005,
     _OrthancPluginService_GetInstanceMetadata = 4006,
+    _OrthancPluginService_GetInstanceOrigin = 4007,
 
     /* Services for plugins implementing a database back-end */
     _OrthancPluginService_RegisterDatabaseBackend = 5000,
@@ -459,7 +501,40 @@
     _OrthancPluginService_GetFontsCount = 6009,
     _OrthancPluginService_GetFontInfo = 6010,
     _OrthancPluginService_DrawText = 6011,
-
+    _OrthancPluginService_CreateImage = 6012,
+    _OrthancPluginService_CreateImageAccessor = 6013,
+    _OrthancPluginService_DecodeDicomImage = 6014,
+
+    /* Primitives for handling C-Find, C-Move and worklists */
+    _OrthancPluginService_WorklistAddAnswer = 7000,
+    _OrthancPluginService_WorklistMarkIncomplete = 7001,
+    _OrthancPluginService_WorklistIsMatch = 7002,
+    _OrthancPluginService_WorklistGetDicomQuery = 7003,
+    _OrthancPluginService_FindAddAnswer = 7004,
+    _OrthancPluginService_FindMarkIncomplete = 7005,
+    _OrthancPluginService_GetFindQuerySize = 7006,
+    _OrthancPluginService_GetFindQueryTag = 7007,
+    _OrthancPluginService_GetFindQueryTagName = 7008,
+    _OrthancPluginService_GetFindQueryValue = 7009,
+    _OrthancPluginService_CreateFindMatcher = 7010,
+    _OrthancPluginService_FreeFindMatcher = 7011,
+    _OrthancPluginService_FindMatcherIsMatch = 7012,
+
+    /* Primitives for accessing Orthanc Peers (new in 1.4.2) */
+    _OrthancPluginService_GetPeers = 8000,
+    _OrthancPluginService_FreePeers = 8001,
+    _OrthancPluginService_GetPeersCount = 8003,
+    _OrthancPluginService_GetPeerName = 8004,
+    _OrthancPluginService_GetPeerUrl = 8005,
+    _OrthancPluginService_CallPeerApi = 8006,
+    _OrthancPluginService_GetPeerUserProperty = 8007,
+
+    /* Primitives for handling jobs (new in 1.4.2) */
+    _OrthancPluginService_CreateJob = 9000,
+    _OrthancPluginService_FreeJob = 9001,
+    _OrthancPluginService_SubmitJob = 9002,
+    _OrthancPluginService_RegisterJobsUnserializer = 9003,
+    
     _OrthancPluginService_INTERNAL = 0x7fffffff
   } _OrthancPluginService;
 
@@ -523,6 +598,46 @@
 
     OrthancPluginPixelFormat_Unknown = 6,   /*!< Unknown pixel format */
 
+    /**
+     * @brief Color image in RGB48 format.
+     *
+     * This format describes a color image. The pixels are stored in 6
+     * consecutive bytes. The memory layout is RRGGBB.
+     **/
+    OrthancPluginPixelFormat_RGB48 = 7,
+
+    /**
+     * @brief Graylevel, unsigned 32bpp image.
+     *
+     * The image is graylevel. Each pixel is unsigned and stored in
+     * four bytes.
+     **/
+    OrthancPluginPixelFormat_Grayscale32 = 8,
+
+    /**
+     * @brief Graylevel, floating-point 32bpp image.
+     *
+     * The image is graylevel. Each pixel is floating-point and stored
+     * in four bytes.
+     **/
+    OrthancPluginPixelFormat_Float32 = 9,
+
+    /**
+     * @brief Color image in BGRA32 format.
+     *
+     * This format describes a color image. The pixels are stored in 4
+     * consecutive bytes. The memory layout is BGRA.
+     **/
+    OrthancPluginPixelFormat_BGRA32 = 10,
+
+    /**
+     * @brief Graylevel, unsigned 64bpp image.
+     *
+     * The image is graylevel. Each pixel is unsigned and stored in
+     * eight bytes.
+     **/
+    OrthancPluginPixelFormat_Grayscale64 = 11,
+
     _OrthancPluginPixelFormat_INTERNAL = 0x7fffffff
   } OrthancPluginPixelFormat;
 
@@ -576,6 +691,10 @@
     OrthancPluginChangeType_StableStudy = 9,        /*!< Timeout: No new instance in this study */
     OrthancPluginChangeType_OrthancStarted = 10,    /*!< Orthanc has started */
     OrthancPluginChangeType_OrthancStopped = 11,    /*!< Orthanc is stopping */
+    OrthancPluginChangeType_UpdatedAttachment = 12, /*!< Some user-defined attachment has changed for this resource */
+    OrthancPluginChangeType_UpdatedMetadata = 13,   /*!< Some user-defined metadata has changed for this resource */
+    OrthancPluginChangeType_UpdatedPeers = 14,      /*!< The list of Orthanc peers has changed */
+    OrthancPluginChangeType_UpdatedModalities = 15, /*!< The list of DICOM modalities has changed */
 
     _OrthancPluginChangeType_INTERNAL = 0x7fffffff
   } OrthancPluginChangeType;
@@ -602,8 +721,9 @@
    **/
   typedef enum
   {
-    OrthancPluginImageFormat_Png = 0,   /*!< Image compressed using PNG */
-    OrthancPluginImageFormat_Jpeg = 1,  /*!< Image compressed using JPEG */
+    OrthancPluginImageFormat_Png = 0,    /*!< Image compressed using PNG */
+    OrthancPluginImageFormat_Jpeg = 1,   /*!< Image compressed using JPEG */
+    OrthancPluginImageFormat_Dicom = 2,  /*!< Image compressed using DICOM */
 
     _OrthancPluginImageFormat_INTERNAL = 0x7fffffff
   } OrthancPluginImageFormat;
@@ -650,12 +770,13 @@
   /**
    * The possible output formats for a DICOM-to-JSON conversion.
    * @ingroup Toolbox
+   * @see OrthancPluginDicomToJson()
    **/
   typedef enum
   {
     OrthancPluginDicomToJsonFormat_Full = 1,    /*!< Full output, with most details */
     OrthancPluginDicomToJsonFormat_Short = 2,   /*!< Tags output as hexadecimal numbers */
-    OrthancPluginDicomToJsonFormat_Simple = 3,  /*!< Human-readable JSON */
+    OrthancPluginDicomToJsonFormat_Human = 3,   /*!< Human-readable JSON */
 
     _OrthancPluginDicomToJsonFormat_INTERNAL = 0x7fffffff
   } OrthancPluginDicomToJsonFormat;
@@ -668,6 +789,7 @@
    **/
   typedef enum
   {
+    OrthancPluginDicomToJsonFlags_None                  = 0,
     OrthancPluginDicomToJsonFlags_IncludeBinary         = (1 << 0),  /*!< Include the binary tags */
     OrthancPluginDicomToJsonFlags_IncludePrivateTags    = (1 << 1),  /*!< Include the private tags */
     OrthancPluginDicomToJsonFlags_IncludeUnknownTags    = (1 << 2),  /*!< Include the tags unknown by the dictionary */
@@ -680,20 +802,75 @@
 
 
   /**
+   * Flags to the creation of a DICOM file.
+   * @ingroup Toolbox
+   * @see OrthancPluginCreateDicom()
+   **/
+  typedef enum
+  {
+    OrthancPluginCreateDicomFlags_None                  = 0,
+    OrthancPluginCreateDicomFlags_DecodeDataUriScheme   = (1 << 0),  /*!< Decode fields encoded using data URI scheme */
+    OrthancPluginCreateDicomFlags_GenerateIdentifiers   = (1 << 1),  /*!< Automatically generate DICOM identifiers */
+
+    _OrthancPluginCreateDicomFlags_INTERNAL = 0x7fffffff
+  } OrthancPluginCreateDicomFlags;
+
+
+  /**
    * The constraints on the DICOM identifiers that must be supported
    * by the database plugins.
    **/
   typedef enum
   {
-    OrthancPluginIdentifierConstraint_Equal,           /*!< Equal */
-    OrthancPluginIdentifierConstraint_SmallerOrEqual,  /*!< Less or equal */
-    OrthancPluginIdentifierConstraint_GreaterOrEqual,  /*!< More or equal */
-    OrthancPluginIdentifierConstraint_Wildcard,        /*!< Case-sensitive wildcard matching (with * and ?) */
+    OrthancPluginIdentifierConstraint_Equal = 1,           /*!< Equal */
+    OrthancPluginIdentifierConstraint_SmallerOrEqual = 2,  /*!< Less or equal */
+    OrthancPluginIdentifierConstraint_GreaterOrEqual = 3,  /*!< More or equal */
+    OrthancPluginIdentifierConstraint_Wildcard = 4,        /*!< Case-sensitive wildcard matching (with * and ?) */
 
     _OrthancPluginIdentifierConstraint_INTERNAL = 0x7fffffff
   } OrthancPluginIdentifierConstraint;
 
 
+  /**
+   * The origin of a DICOM instance that has been received by Orthanc.
+   **/
+  typedef enum
+  {
+    OrthancPluginInstanceOrigin_Unknown = 1,        /*!< Unknown origin */
+    OrthancPluginInstanceOrigin_DicomProtocol = 2,  /*!< Instance received through DICOM protocol */
+    OrthancPluginInstanceOrigin_RestApi = 3,        /*!< Instance received through REST API of Orthanc */
+    OrthancPluginInstanceOrigin_Plugin = 4,         /*!< Instance added to Orthanc by a plugin */
+    OrthancPluginInstanceOrigin_Lua = 5,            /*!< Instance added to Orthanc by a Lua script */
+
+    _OrthancPluginInstanceOrigin_INTERNAL = 0x7fffffff
+  } OrthancPluginInstanceOrigin;
+
+
+  /**
+   * The possible status for one single step of a job.
+   **/
+  typedef enum
+  {
+    OrthancPluginJobStepStatus_Success = 1,   /*!< The job has successfully executed all its steps */
+    OrthancPluginJobStepStatus_Failure = 2,   /*!< The job has failed while executing this step */
+    OrthancPluginJobStepStatus_Continue = 3   /*!< The job has still data to process after this step */
+  } OrthancPluginJobStepStatus;
+
+
+  /**
+   * Explains why the job should stop and release the resources it has
+   * allocated. This is especially important to disambiguate between
+   * the "paused" condition and the "final" conditions (success,
+   * failure, or canceled).
+   **/
+  typedef enum
+  {
+    OrthancPluginJobStopReason_Success = 1,  /*!< The job has succeeded */
+    OrthancPluginJobStopReason_Paused = 2,   /*!< The job was paused, and will be resumed later */
+    OrthancPluginJobStopReason_Failure = 3,  /*!< The job has failed, and might be resubmitted later */
+    OrthancPluginJobStopReason_Canceled = 4  /*!< The job was canceled, and might be resubmitted later */
+  } OrthancPluginJobStopReason;
+
 
   /**
    * @brief A memory buffer allocated by the core system of Orthanc.
@@ -750,6 +927,62 @@
 
 
   /**
+   * @brief Opaque structure to an object that represents a C-Find query for worklists.
+   * @ingroup DicomCallbacks
+   **/
+  typedef struct _OrthancPluginWorklistQuery_t OrthancPluginWorklistQuery;
+
+
+
+  /**
+   * @brief Opaque structure to an object that represents the answers to a C-Find query for worklists.
+   * @ingroup DicomCallbacks
+   **/
+  typedef struct _OrthancPluginWorklistAnswers_t OrthancPluginWorklistAnswers;
+
+
+
+  /**
+   * @brief Opaque structure to an object that represents a C-Find query.
+   * @ingroup DicomCallbacks
+   **/
+  typedef struct _OrthancPluginFindQuery_t OrthancPluginFindQuery;
+
+
+
+  /**
+   * @brief Opaque structure to an object that represents the answers to a C-Find query for worklists.
+   * @ingroup DicomCallbacks
+   **/
+  typedef struct _OrthancPluginFindAnswers_t OrthancPluginFindAnswers;
+
+
+
+  /**
+   * @brief Opaque structure to an object that can be used to check whether a DICOM instance matches a C-Find query.
+   * @ingroup Toolbox
+   **/
+  typedef struct _OrthancPluginFindAnswers_t OrthancPluginFindMatcher;
+
+
+  
+  /**
+   * @brief Opaque structure to the set of remote Orthanc Peers that are known to the local Orthanc server.
+   * @ingroup Toolbox
+   **/
+  typedef struct _OrthancPluginPeers_t OrthancPluginPeers;
+
+
+
+  /**
+   * @brief Opaque structure to a job to be executed by Orthanc.
+   * @ingroup Toolbox
+   **/
+  typedef struct _OrthancPluginJob_t OrthancPluginJob;  
+
+  
+
+  /**
    * @brief Signature of a callback function that answers to a REST request.
    * @ingroup Callbacks
    **/
@@ -782,6 +1015,18 @@
 
 
   /**
+   * @brief Signature of a callback function to decode a DICOM instance as an image.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginDecodeImageCallback) (
+    OrthancPluginImage** target,
+    const void* dicom,
+    const uint32_t size,
+    uint32_t frameIndex);
+
+
+
+  /**
    * @brief Signature of a function to free dynamic memory.
    **/
   typedef void (*OrthancPluginFree) (void* buffer);
@@ -845,6 +1090,337 @@
 
 
   /**
+   * @brief Callback to handle the C-Find SCP requests for worklists.
+   *
+   * Signature of a callback function that is triggered when Orthanc
+   * receives a C-Find SCP request against modality worklists.
+   *
+   * @param answers The target structure where answers must be stored.
+   * @param query The worklist query.
+   * @param issuerAet The Application Entity Title (AET) of the modality from which the request originates.
+   * @param calledAet The Application Entity Title (AET) of the modality that is called by the request.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWorklistCallback) (
+    OrthancPluginWorklistAnswers*     answers,
+    const OrthancPluginWorklistQuery* query,
+    const char*                       issuerAet,
+    const char*                       calledAet);
+
+
+
+  /**
+   * @brief Callback to filter incoming HTTP requests received by Orthanc.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives an HTTP/REST request, and that answers whether
+   * this request should be allowed. If the callback returns "0"
+   * ("false"), the server answers with HTTP status code 403
+   * (Forbidden).
+   *
+   * @param method The HTTP method used by the request.
+   * @param uri The URI of interest.
+   * @param ip The IP address of the HTTP client.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys The keys of the HTTP headers (always converted to low-case).
+   * @param headersValues The values of the HTTP headers.
+   * @return 0 if forbidden access, 1 if allowed access, -1 if error.
+   * @ingroup Callback
+   * @deprecated Please instead use OrthancPluginIncomingHttpRequestFilter2()
+   **/
+  typedef int32_t (*OrthancPluginIncomingHttpRequestFilter) (
+    OrthancPluginHttpMethod  method,
+    const char*              uri,
+    const char*              ip,
+    uint32_t                 headersCount,
+    const char* const*       headersKeys,
+    const char* const*       headersValues);
+
+
+
+  /**
+   * @brief Callback to filter incoming HTTP requests received by Orthanc.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives an HTTP/REST request, and that answers whether
+   * this request should be allowed. If the callback returns "0"
+   * ("false"), the server answers with HTTP status code 403
+   * (Forbidden).
+   *
+   * @param method The HTTP method used by the request.
+   * @param uri The URI of interest.
+   * @param ip The IP address of the HTTP client.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys The keys of the HTTP headers (always converted to low-case).
+   * @param headersValues The values of the HTTP headers.
+   * @param getArgumentsCount The number of GET arguments (only for the GET HTTP method).
+   * @param getArgumentsKeys The keys of the GET arguments (only for the GET HTTP method).
+   * @param getArgumentsValues The values of the GET arguments (only for the GET HTTP method).
+   * @return 0 if forbidden access, 1 if allowed access, -1 if error.
+   * @ingroup Callback
+   **/
+  typedef int32_t (*OrthancPluginIncomingHttpRequestFilter2) (
+    OrthancPluginHttpMethod  method,
+    const char*              uri,
+    const char*              ip,
+    uint32_t                 headersCount,
+    const char* const*       headersKeys,
+    const char* const*       headersValues,
+    uint32_t                 getArgumentsCount,
+    const char* const*       getArgumentsKeys,
+    const char* const*       getArgumentsValues);
+
+
+
+  /**
+   * @brief Callback to handle incoming C-Find SCP requests.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives a C-Find SCP request not concerning modality
+   * worklists.
+   *
+   * @param answers The target structure where answers must be stored.
+   * @param query The worklist query.
+   * @param issuerAet The Application Entity Title (AET) of the modality from which the request originates.
+   * @param calledAet The Application Entity Title (AET) of the modality that is called by the request.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginFindCallback) (
+    OrthancPluginFindAnswers*     answers,
+    const OrthancPluginFindQuery* query,
+    const char*                   issuerAet,
+    const char*                   calledAet);
+
+
+
+  /**
+   * @brief Callback to handle incoming C-Move SCP requests.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives a C-Move SCP request. The callback receives the
+   * type of the resource of interest (study, series, instance...)
+   * together with the DICOM tags containing its identifiers. In turn,
+   * the plugin must create a driver object that will be responsible
+   * for driving the successive move suboperations.
+   *
+   * @param resourceType The type of the resource of interest. Note
+   * that this might be set to ResourceType_None if the
+   * QueryRetrieveLevel (0008,0052) tag was not provided by the
+   * issuer (i.e. the originator modality).
+   * @param patientId Content of the PatientID (0x0010, 0x0020) tag of the resource of interest. Might be NULL.
+   * @param accessionNumber Content of the AccessionNumber (0x0008, 0x0050) tag. Might be NULL.
+   * @param studyInstanceUid Content of the StudyInstanceUID (0x0020, 0x000d) tag. Might be NULL.
+   * @param seriesInstanceUid Content of the SeriesInstanceUID (0x0020, 0x000e) tag. Might be NULL.
+   * @param sopInstanceUid Content of the SOPInstanceUID (0x0008, 0x0018) tag. Might be NULL.
+   * @param originatorAet The Application Entity Title (AET) of the
+   * modality from which the request originates.
+   * @param sourceAet The Application Entity Title (AET) of the
+   * modality that should send its DICOM files to another modality.
+   * @param targetAet The Application Entity Title (AET) of the
+   * modality that should receive the DICOM files.
+   * @param originatorId The Message ID issued by the originator modality,
+   * as found in tag (0000,0110) of the DICOM query emitted by the issuer.
+   *
+   * @return The NULL value if the plugin cannot deal with this query,
+   * or a pointer to the driver object that is responsible for
+   * handling the successive move suboperations.
+   * 
+   * @note If targetAet equals sourceAet, this is actually a query/retrieve operation.
+   * @ingroup DicomCallbacks
+   **/
+  typedef void* (*OrthancPluginMoveCallback) (
+    OrthancPluginResourceType  resourceType,
+    const char*                patientId,
+    const char*                accessionNumber,
+    const char*                studyInstanceUid,
+    const char*                seriesInstanceUid,
+    const char*                sopInstanceUid,
+    const char*                originatorAet,
+    const char*                sourceAet,
+    const char*                targetAet,
+    uint16_t                   originatorId);
+    
+
+  /**
+   * @brief Callback to read the size of a C-Move driver.
+   * 
+   * Signature of a callback function that returns the number of
+   * C-Move suboperations that are to be achieved by the given C-Move
+   * driver. This driver is the return value of a previous call to the
+   * OrthancPluginMoveCallback() callback.
+   *
+   * @param moveDriver The C-Move driver of interest.
+   * @return The number of suboperations. 
+   * @ingroup DicomCallbacks
+   **/
+  typedef uint32_t (*OrthancPluginGetMoveSize) (void* moveDriver);
+
+
+  /**
+   * @brief Callback to apply one C-Move suboperation.
+   * 
+   * Signature of a callback function that applies the next C-Move
+   * suboperation that os to be achieved by the given C-Move
+   * driver. This driver is the return value of a previous call to the
+   * OrthancPluginMoveCallback() callback.
+   *
+   * @param moveDriver The C-Move driver of interest.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup DicomCallbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginApplyMove) (void* moveDriver);
+
+
+  /**
+   * @brief Callback to free one C-Move driver.
+   * 
+   * Signature of a callback function that releases the resources
+   * allocated by the given C-Move driver. This driver is the return
+   * value of a previous call to the OrthancPluginMoveCallback()
+   * callback.
+   *
+   * @param moveDriver The C-Move driver of interest.
+   * @ingroup DicomCallbacks
+   **/
+  typedef void (*OrthancPluginFreeMove) (void* moveDriver);
+
+
+  /**
+   * @brief Callback to finalize one custom job.
+   * 
+   * Signature of a callback function that releases all the resources
+   * allocated by the given job. This job is the argument provided to
+   * OrthancPluginCreateJob().
+   *
+   * @param job The job of interest.
+   * @ingroup Toolbox
+   **/  
+  typedef void (*OrthancPluginJobFinalize) (void* job);
+
+
+  /**
+   * @brief Callback to check the progress of one custom job.
+   * 
+   * Signature of a callback function that returns the progress of the
+   * job.
+   *
+   * @param job The job of interest.
+   * @return The progress, as a floating-point number ranging from 0 to 1.
+   * @ingroup Toolbox
+   **/  
+  typedef float (*OrthancPluginJobGetProgress) (void* job);
+
+  
+  /**
+   * @brief Callback to retrieve the content of one custom job.
+   * 
+   * Signature of a callback function that returns human-readable
+   * statistics about the job. This statistics must be formatted as a
+   * JSON object. This information is notably displayed in the "Jobs"
+   * tab of "Orthanc Explorer".
+   *
+   * @param job The job of interest.
+   * @return The statistics, as a JSON object encoded as a string.
+   * @ingroup Toolbox
+   **/  
+  typedef const char* (*OrthancPluginJobGetContent) (void* job);
+
+
+  /**
+   * @brief Callback to serialize one custom job.
+   * 
+   * Signature of a callback function that returns a serialized
+   * version of the job, formatted as a JSON object. This
+   * serialization is stored in the Orthanc database, and is used to
+   * reload the job on the restart of Orthanc. The "unserialization"
+   * callback (with OrthancPluginJobsUnserializer signature) will
+   * receive this serialized object.
+   *
+   * @param job The job of interest.
+   * @return The serialized job, as a JSON object encoded as a string.
+   * @see OrthancPluginRegisterJobsUnserializer()
+   * @ingroup Toolbox
+   **/  
+  typedef const char* (*OrthancPluginJobGetSerialized) (void* job);
+
+
+  /**
+   * @brief Callback to execute one step of a custom job.
+   * 
+   * Signature of a callback function that executes one step in the
+   * job. The jobs engine of Orthanc will make successive calls to
+   * this method, as long as it returns
+   * OrthancPluginJobStepStatus_Continue.
+   *
+   * @param job The job of interest.
+   * @return The status of execution.
+   * @ingroup Toolbox
+   **/  
+  typedef OrthancPluginJobStepStatus (*OrthancPluginJobStep) (void* job);
+
+
+  /**
+   * @brief Callback executed once one custom job leaves the "running" state.
+   * 
+   * Signature of a callback function that is invoked once a job
+   * leaves the "running" state. This can happen if the previous call
+   * to OrthancPluginJobStep has failed/succeeded, if the host Orthanc
+   * server is being stopped, or if the user manually tags the job as
+   * paused/canceled. This callback allows the plugin to free
+   * resources allocated for running this custom job (e.g. to stop
+   * threads, or to remove temporary files).
+   * 
+   * Note that handling pauses might involves a specific treatment
+   * (such a stopping threads, but keeping temporary files on the
+   * disk). This "paused" situation can be checked by looking at the
+   * "reason" parameter.
+   *
+   * @param job The job of interest.
+   * @param reason The reason for leaving the "running" state. 
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/    
+  typedef OrthancPluginErrorCode (*OrthancPluginJobStop) (void* job, 
+                                                          OrthancPluginJobStopReason reason);
+
+
+  /**
+   * @brief Callback executed once one stopped custom job is started again.
+   * 
+   * Signature of a callback function that is invoked once a job
+   * leaves the "failure/canceled" state, to be started again. This
+   * function will typically reset the progress to zero. Note that
+   * before being actually executed, the job would first be tagged as
+   * "pending" in the Orthanc jobs engine.
+   *
+   * @param job The job of interest.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/    
+  typedef OrthancPluginErrorCode (*OrthancPluginJobReset) (void* job);
+
+
+  /**
+   * @brief Callback executed to unserialized a custom job.
+   * 
+   * Signature of a callback function that unserializes a job that was
+   * saved in the Orthanc database.
+   *
+   * @param jobType The type of the job, as provided to OrthancPluginCreateJob().
+   * @param serialized The serialization of the job, as provided by OrthancPluginJobGetSerialized.
+   * @return The unserialized job (as created by OrthancPluginCreateJob()), or NULL
+   * if this unserializer cannot handle this job type.
+   * @see OrthancPluginRegisterJobsUnserializer()
+   * @ingroup Callbacks
+   **/    
+  typedef OrthancPluginJob* (*OrthancPluginJobsUnserializer) (const char* jobType,
+                                                              const char* serialized);
+  
+
+
+  /**
    * @brief Data structure that contains information about the Orthanc core.
    **/
   typedef struct _OrthancPluginContext_t
@@ -858,6 +1434,20 @@
   } OrthancPluginContext;
 
 
+  
+  /**
+   * @brief An entry in the dictionary of DICOM tags.
+   **/
+  typedef struct
+  {
+    uint16_t                          group;            /*!< The group of the tag */
+    uint16_t                          element;          /*!< The element of the tag */
+    OrthancPluginValueRepresentation  vr;               /*!< The value representation of the tag */
+    uint32_t                          minMultiplicity;  /*!< The minimum multiplicity of the tag */
+    uint32_t                          maxMultiplicity;  /*!< The maximum multiplicity of the tag (0 means arbitrary) */
+  } OrthancPluginDictionaryEntry;
+
+
 
   /**
    * @brief Free a string.
@@ -879,20 +1469,29 @@
 
 
   /**
-   * @brief Check the compatibility of the plugin wrt. the version of its hosting Orthanc.
+   * @brief Check that the version of the hosting Orthanc is above a given version.
    * 
-   * This function checks whether the version of this C header is
-   * compatible with the current version of Orthanc. The result of
-   * this function should always be checked in the
-   * OrthancPluginInitialize() entry point of the plugin.
+   * This function checks whether the version of the Orthanc server
+   * running this plugin, is above the given version. Contrarily to
+   * OrthancPluginCheckVersion(), it is up to the developer of the
+   * plugin to make sure that all the Orthanc SDK services called by
+   * the plugin are actually implemented in the given version of
+   * Orthanc.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param expectedMajor Expected major version.
+   * @param expectedMinor Expected minor version.
+   * @param expectedRevision Expected revision.
    * @return 1 if and only if the versions are compatible. If the
    * result is 0, the initialization of the plugin should fail.
+   * @see OrthancPluginCheckVersion
    * @ingroup Callbacks
    **/
-  ORTHANC_PLUGIN_INLINE int  OrthancPluginCheckVersion(
-    OrthancPluginContext* context)
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginCheckVersionAdvanced(
+    OrthancPluginContext* context,
+    int expectedMajor,
+    int expectedMinor,
+    int expectedRevision)
   {
     int major, minor, revision;
 
@@ -909,7 +1508,10 @@
         sizeof(int32_t) != sizeof(OrthancPluginValueRepresentation) ||
         sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) ||
         sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) ||
-        sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint))
+        sizeof(int32_t) != sizeof(OrthancPluginCreateDicomFlags) ||
+        sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) ||
+        sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) ||
+        sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus))
     {
       /* Mismatch in the size of the enumerations */
       return 0;
@@ -935,31 +1537,31 @@
 
     /* Check the major number of the version */
 
-    if (major > ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER)
+    if (major > expectedMajor)
     {
       return 1;
     }
 
-    if (major < ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER)
+    if (major < expectedMajor)
     {
       return 0;
     }
 
     /* Check the minor number of the version */
 
-    if (minor > ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER)
+    if (minor > expectedMinor)
     {
       return 1;
     }
 
-    if (minor < ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER)
+    if (minor < expectedMinor)
     {
       return 0;
     }
 
     /* Check the revision number of the version */
 
-    if (revision >= ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER)
+    if (revision >= expectedRevision)
     {
       return 1;
     }
@@ -971,6 +1573,33 @@
 
 
   /**
+   * @brief Check the compatibility of the plugin wrt. the version of its hosting Orthanc.
+   * 
+   * This function checks whether the version of the Orthanc server
+   * running this plugin, is above the version of the current Orthanc
+   * SDK header. This guarantees that the plugin is compatible with
+   * the hosting Orthanc (i.e. it will not call unavailable services).
+   * The result of this function should always be checked in the
+   * OrthancPluginInitialize() entry point of the plugin.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return 1 if and only if the versions are compatible. If the
+   * result is 0, the initialization of the plugin should fail.
+   * @see OrthancPluginCheckVersionAdvanced
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginCheckVersion(
+    OrthancPluginContext* context)
+  {
+    return OrthancPluginCheckVersionAdvanced(
+      context,
+      ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+      ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+      ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+  }
+
+
+  /**
    * @brief Free a memory buffer.
    * 
    * Free a memory buffer that was allocated by the core system of Orthanc.
@@ -1244,7 +1873,7 @@
    * file is stored into a newly allocated memory buffer.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer.
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param instanceId The Orthanc identifier of the DICOM instance of interest.
    * @return 0 if success, or the error code if failure.
    * @ingroup Orthanc
@@ -1275,9 +1904,10 @@
    * the query is stored into a newly allocated memory buffer.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer.
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param uri The URI in the built-in Orthanc API.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiGetAfterPlugins
    * @ingroup Orthanc
    **/
@@ -1304,9 +1934,10 @@
    * query is stored into a newly allocated memory buffer.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer.
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param uri The URI in the built-in Orthanc API.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiGet
    * @ingroup Orthanc
    **/
@@ -1338,11 +1969,12 @@
    * the query is stored into a newly allocated memory buffer.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer.
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param uri The URI in the built-in Orthanc API.
    * @param body The body of the POST request.
    * @param bodySize The size of the body.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiPostAfterPlugins
    * @ingroup Orthanc
    **/
@@ -1372,11 +2004,12 @@
    * query is stored into a newly allocated memory buffer.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer.
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param uri The URI in the built-in Orthanc API.
    * @param body The body of the POST request.
    * @param bodySize The size of the body.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiPost
    * @ingroup Orthanc
    **/
@@ -1405,6 +2038,7 @@
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param uri The URI to delete in the built-in Orthanc API.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiDeleteAfterPlugins
    * @ingroup Orthanc
    **/
@@ -1427,6 +2061,7 @@
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param uri The URI to delete in the built-in Orthanc API.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiDelete
    * @ingroup Orthanc
    **/
@@ -1446,11 +2081,12 @@
    * the query is stored into a newly allocated memory buffer.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer.
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param uri The URI in the built-in Orthanc API.
    * @param body The body of the PUT request.
    * @param bodySize The size of the body.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiPutAfterPlugins
    * @ingroup Orthanc
    **/
@@ -1481,11 +2117,12 @@
    * query is stored into a newly allocated memory buffer.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer.
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param uri The URI in the built-in Orthanc API.
    * @param body The body of the PUT request.
    * @param bodySize The size of the body.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiPut
    * @ingroup Orthanc
    **/
@@ -1857,11 +2494,12 @@
 
   typedef struct
   {
-    char**                      resultStringToFree;
-    const char**                resultString;
-    int64_t*                    resultInt64;
-    const char*                 key;
-    OrthancPluginDicomInstance* instance;
+    char**                       resultStringToFree;
+    const char**                 resultString;
+    int64_t*                     resultInt64;
+    const char*                  key;
+    OrthancPluginDicomInstance*  instance;
+    OrthancPluginInstanceOrigin* resultOrigin;   /* New in Orthanc 0.9.5 SDK */
   } _OrthancPluginAccessDicomInstance;
 
 
@@ -2274,7 +2912,7 @@
    * Orthanc, you should make these calls in a separate thread (with
    * the events passing through a message queue). Otherwise, this
    * could result in deadlocks in the presence of other plugins or Lua
-   * script.
+   * scripts.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param callback The callback function.
@@ -2524,7 +3162,6 @@
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @return The version.
    * @ingroup Callbacks
-   * @deprecated Please instead use IDatabaseBackend::UpgradeDatabase()
    **/
   ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetExpectedDatabaseVersion(
     OrthancPluginContext*  context)
@@ -2597,7 +3234,7 @@
    * @param subType The sub-type of the multipart answer ("mixed" or "related").
    * @param contentType The MIME type of the items in the multipart answer.
    * @return 0 if success, or the error code if failure.
-   * @see OrthancPluginSendMultipartItem()
+   * @see OrthancPluginSendMultipartItem(), OrthancPluginSendMultipartItem2()
    * @ingroup REST
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStartMultipartAnswer(
@@ -2626,6 +3263,7 @@
    * @param answerSize Number of bytes of the item.
    * @return 0 if success, or the error code if failure (this notably happens
    * if the connection is closed by the client).
+   * @see OrthancPluginSendMultipartItem2()
    * @ingroup REST
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendMultipartItem(
@@ -2661,7 +3299,7 @@
    * version of the zlib library that is used by the Orthanc core.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer.
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param source The source buffer.
    * @param size The size in bytes of the source buffer.
    * @param compression The compression algorithm.
@@ -2703,7 +3341,7 @@
    * a newly allocated memory buffer.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer.
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param path The path of the file to be read.
    * @return 0 if success, or the error code if failure.
    **/
@@ -2844,7 +3482,7 @@
     const OrthancPluginImage*  image;
     uint32_t*                  resultUint32;
     OrthancPluginPixelFormat*  resultPixelFormat;
-    const void**               resultBuffer;
+    void**                     resultBuffer;
   } _OrthancPluginGetImageInfo;
 
 
@@ -2993,11 +3631,11 @@
    * @return The pointer.
    * @ingroup Images
    **/
-  ORTHANC_PLUGIN_INLINE const void*  OrthancPluginGetImageBuffer(
+  ORTHANC_PLUGIN_INLINE void*  OrthancPluginGetImageBuffer(
     OrthancPluginContext*      context,
     const OrthancPluginImage*  image)
   {
-    const void* target = NULL;
+    void* target = NULL;
 
     _OrthancPluginGetImageInfo params;
     memset(&params, 0, sizeof(params));
@@ -3261,7 +3899,7 @@
    * Orthanc instance that hosts this plugin.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer.
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param url The URL of interest.
    * @param username The username (can be <tt>NULL</tt> if no password protection).
    * @param password The password (can be <tt>NULL</tt> if no password protection).
@@ -3296,7 +3934,7 @@
    * the Orthanc instance that hosts this plugin.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer.
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param url The URL of interest.
    * @param body The content of the body of the request.
    * @param bodySize The size of the body of the request.
@@ -3337,7 +3975,7 @@
    * Orthanc instance that hosts this plugin.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer.
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param url The URL of interest.
    * @param body The content of the body of the request.
    * @param bodySize The size of the body of the request.
@@ -3790,9 +4428,9 @@
   /**
    * @brief Register a new tag into the DICOM dictionary.
    *
-   * This function declares a new tag in the dictionary of DICOM tags
-   * that is known to Orthanc. This function should be used in the
-   * OrthancPluginInitialize() callback.
+   * This function declares a new public tag in the dictionary of
+   * DICOM tags that are known to Orthanc. This function should be
+   * used in the OrthancPluginInitialize() callback.
    *
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param group The group of the tag.
@@ -3803,6 +4441,7 @@
    * @param maxMultiplicity The maximum multiplicity of the tag. A value of 0 means
    * an arbitrary multiplicity ("<tt>n</tt>").
    * @return 0 if success, other value if error.
+   * @see OrthancPluginRegisterPrivateDictionaryTag()
    * @ingroup Toolbox
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRegisterDictionaryTag(
@@ -3827,6 +4466,60 @@
 
 
 
+  typedef struct
+  {
+    uint16_t                          group;
+    uint16_t                          element;
+    OrthancPluginValueRepresentation  vr;
+    const char*                       name;
+    uint32_t                          minMultiplicity;
+    uint32_t                          maxMultiplicity;
+    const char*                       privateCreator;
+  } _OrthancPluginRegisterPrivateDictionaryTag;
+  
+  /**
+   * @brief Register a new private tag into the DICOM dictionary.
+   *
+   * This function declares a new private tag in the dictionary of
+   * DICOM tags that are known to Orthanc. This function should be
+   * used in the OrthancPluginInitialize() callback.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param group The group of the tag.
+   * @param element The element of the tag.
+   * @param vr The value representation of the tag.
+   * @param name The nickname of the tag.
+   * @param minMultiplicity The minimum multiplicity of the tag (must be above 0).
+   * @param maxMultiplicity The maximum multiplicity of the tag. A value of 0 means
+   * an arbitrary multiplicity ("<tt>n</tt>").
+   * @param privateCreator The private creator of this private tag.
+   * @return 0 if success, other value if error.
+   * @see OrthancPluginRegisterDictionaryTag()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRegisterPrivateDictionaryTag(
+    OrthancPluginContext*             context,
+    uint16_t                          group,
+    uint16_t                          element,
+    OrthancPluginValueRepresentation  vr,
+    const char*                       name,
+    uint32_t                          minMultiplicity,
+    uint32_t                          maxMultiplicity,
+    const char*                       privateCreator)
+  {
+    _OrthancPluginRegisterPrivateDictionaryTag params;
+    params.group = group;
+    params.element = element;
+    params.vr = vr;
+    params.name = name;
+    params.minMultiplicity = minMultiplicity;
+    params.maxMultiplicity = maxMultiplicity;
+    params.privateCreator = privateCreator;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterPrivateDictionaryTag, &params);
+  }
+
+
 
   typedef struct
   {
@@ -3840,9 +4533,7 @@
    * This function requests the Orthanc core to reconstruct the main
    * DICOM tags of all the resources of the given type. This function
    * can only be used as a part of the upgrade of a custom database
-   * back-end
-   * (cf. OrthancPlugins::IDatabaseBackend::UpgradeDatabase). A
-   * database transaction will be automatically setup.
+   * back-end. A database transaction will be automatically setup.
    *
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param storageArea The storage area.
@@ -3867,7 +4558,7 @@
   {
     char**                          result;
     const char*                     instanceId;
-    const char*                     buffer;
+    const void*                     buffer;
     uint32_t                        size;
     OrthancPluginDicomToJsonFormat  format;
     OrthancPluginDicomToJsonFlags   flags;
@@ -3886,7 +4577,7 @@
    * @param buffer The memory buffer containing the DICOM file.
    * @param size The size of the memory buffer.
    * @param format The output format.
-   * @param flags The output flags.
+   * @param flags Flags governing the output.
    * @param maxStringLength The maximum length of a field. Too long fields will
    * be output as "null". The 0 value means no maximum length.
    * @return The NULL value if the case of an error, or the JSON
@@ -3896,7 +4587,7 @@
    **/
   ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomBufferToJson(
     OrthancPluginContext*           context,
-    const char*                     buffer,
+    const void*                     buffer,
     uint32_t                        size,
     OrthancPluginDicomToJsonFormat  format,
     OrthancPluginDicomToJsonFlags   flags, 
@@ -3935,7 +4626,7 @@
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param instanceId The Orthanc identifier of the instance.
    * @param format The output format.
-   * @param flags The output flags.
+   * @param flags Flags governing the output.
    * @param maxStringLength The maximum length of a field. Too long fields will
    * be output as "null". The 0 value means no maximum length.
    * @return The NULL value if the case of an error, or the JSON
@@ -3972,6 +4663,1759 @@
   }
 
 
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    const char*                 uri;
+    uint32_t                    headersCount;
+    const char* const*          headersKeys;
+    const char* const*          headersValues;
+    int32_t                     afterPlugins;
+  } _OrthancPluginRestApiGet2;
+
+  /**
+   * @brief Make a GET call to the Orthanc REST API, with custom HTTP headers.
+   * 
+   * Make a GET call to the Orthanc REST API with extended
+   * parameters. The result to the query is stored into a newly
+   * allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param uri The URI in the built-in Orthanc API.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys Array containing the keys of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param headersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param afterPlugins If 0, the built-in API of Orthanc is used.
+   * If 1, the API is tainted by the plugins.
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginRestApiGet, OrthancPluginRestApiGetAfterPlugins
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiGet2(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    uint32_t                    headersCount,
+    const char* const*          headersKeys,
+    const char* const*          headersValues,
+    int32_t                     afterPlugins)
+  {
+    _OrthancPluginRestApiGet2 params;
+    params.target = target;
+    params.uri = uri;
+    params.headersCount = headersCount;
+    params.headersKeys = headersKeys;
+    params.headersValues = headersValues;
+    params.afterPlugins = afterPlugins;
+
+    return context->InvokeService(context, _OrthancPluginService_RestApiGet2, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginWorklistCallback callback;
+  } _OrthancPluginWorklistCallback;
+
+  /**
+   * @brief Register a callback to handle modality worklists requests.
+   *
+   * This function registers a callback to handle C-Find SCP requests
+   * on modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterWorklistCallback(
+    OrthancPluginContext*          context,
+    OrthancPluginWorklistCallback  callback)
+  {
+    _OrthancPluginWorklistCallback params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterWorklistCallback, &params);
+  }
+
+
+  
+  typedef struct
+  {
+    OrthancPluginWorklistAnswers*      answers;
+    const OrthancPluginWorklistQuery*  query;
+    const void*                        dicom;
+    uint32_t                           size;
+  } _OrthancPluginWorklistAnswersOperation;
+
+  /**
+   * @brief Add one answer to some modality worklist request.
+   *
+   * This function adds one worklist (encoded as a DICOM file) to the
+   * set of answers corresponding to some C-Find SCP request against
+   * modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answers The set of answers.
+   * @param query The worklist query, as received by the callback.
+   * @param dicom The worklist to answer, encoded as a DICOM file.
+   * @param size The size of the DICOM file.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   * @see OrthancPluginCreateDicom()
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistAddAnswer(
+    OrthancPluginContext*             context,
+    OrthancPluginWorklistAnswers*     answers,
+    const OrthancPluginWorklistQuery* query,
+    const void*                       dicom,
+    uint32_t                          size)
+  {
+    _OrthancPluginWorklistAnswersOperation params;
+    params.answers = answers;
+    params.query = query;
+    params.dicom = dicom;
+    params.size = size;
+
+    return context->InvokeService(context, _OrthancPluginService_WorklistAddAnswer, &params);
+  }
+
+
+  /**
+   * @brief Mark the set of worklist answers as incomplete.
+   *
+   * This function marks as incomplete the set of answers
+   * corresponding to some C-Find SCP request against modality
+   * worklists. This must be used if canceling the handling of a
+   * request when too many answers are to be returned.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answers The set of answers.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistMarkIncomplete(
+    OrthancPluginContext*          context,
+    OrthancPluginWorklistAnswers*  answers)
+  {
+    _OrthancPluginWorklistAnswersOperation params;
+    params.answers = answers;
+    params.query = NULL;
+    params.dicom = NULL;
+    params.size = 0;
+
+    return context->InvokeService(context, _OrthancPluginService_WorklistMarkIncomplete, &params);
+  }
+
+
+  typedef struct
+  {
+    const OrthancPluginWorklistQuery*  query;
+    const void*                        dicom;
+    uint32_t                           size;
+    int32_t*                           isMatch;
+    OrthancPluginMemoryBuffer*         target;
+  } _OrthancPluginWorklistQueryOperation;
+
+  /**
+   * @brief Test whether a worklist matches the query.
+   *
+   * This function checks whether one worklist (encoded as a DICOM
+   * file) matches the C-Find SCP query against modality
+   * worklists. This function must be called before adding the
+   * worklist as an answer through OrthancPluginWorklistAddAnswer().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The worklist query, as received by the callback.
+   * @param dicom The worklist to answer, encoded as a DICOM file.
+   * @param size The size of the DICOM file.
+   * @return 1 if the worklist matches the query, 0 otherwise.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginWorklistIsMatch(
+    OrthancPluginContext*              context,
+    const OrthancPluginWorklistQuery*  query,
+    const void*                        dicom,
+    uint32_t                           size)
+  {
+    int32_t isMatch = 0;
+
+    _OrthancPluginWorklistQueryOperation params;
+    params.query = query;
+    params.dicom = dicom;
+    params.size = size;
+    params.isMatch = &isMatch;
+    params.target = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_WorklistIsMatch, &params) == OrthancPluginErrorCode_Success)
+    {
+      return isMatch;
+    }
+    else
+    {
+      /* Error: Assume non-match */
+      return 0;
+    }
+  }
+
+
+  /**
+   * @brief Retrieve the worklist query as a DICOM file.
+   *
+   * This function retrieves the DICOM file that underlies a C-Find
+   * SCP query against modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target Memory buffer where to store the DICOM file. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param query The worklist query, as received by the callback.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistGetDicomQuery(
+    OrthancPluginContext*              context,
+    OrthancPluginMemoryBuffer*         target,
+    const OrthancPluginWorklistQuery*  query)
+  {
+    _OrthancPluginWorklistQueryOperation params;
+    params.query = query;
+    params.dicom = NULL;
+    params.size = 0;
+    params.isMatch = NULL;
+    params.target = target;
+
+    return context->InvokeService(context, _OrthancPluginService_WorklistGetDicomQuery, &params);
+  }
+
+
+  /**
+   * @brief Get the origin of a DICOM file.
+   *
+   * This function returns the origin of a DICOM instance that has been received by Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The origin of the instance.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginInstanceOrigin OrthancPluginGetInstanceOrigin(
+    OrthancPluginContext*       context,
+    OrthancPluginDicomInstance* instance)
+  {
+    OrthancPluginInstanceOrigin origin;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultOrigin = &origin;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceOrigin, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return OrthancPluginInstanceOrigin_Unknown;
+    }
+    else
+    {
+      return origin;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*     target;
+    const char*                    json;
+    const OrthancPluginImage*      pixelData;
+    OrthancPluginCreateDicomFlags  flags;
+  } _OrthancPluginCreateDicom;
+
+  /**
+   * @brief Create a DICOM instance from a JSON string and an image.
+   *
+   * This function takes as input a string containing a JSON file
+   * describing the content of a DICOM instance. As an output, it
+   * writes the corresponding DICOM instance to a newly allocated
+   * memory buffer. Additionally, an image to be encoded within the
+   * DICOM instance can also be provided.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param json The input JSON file.
+   * @param pixelData The image. Can be NULL, if the pixel data is encoded inside the JSON with the data URI scheme.
+   * @param flags Flags governing the output.
+   * @return 0 if success, other value if error.
+   * @ingroup Toolbox
+   * @see OrthancPluginDicomBufferToJson
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateDicom(
+    OrthancPluginContext*          context,
+    OrthancPluginMemoryBuffer*     target,
+    const char*                    json,
+    const OrthancPluginImage*      pixelData,
+    OrthancPluginCreateDicomFlags  flags)
+  {
+    _OrthancPluginCreateDicom params;
+    params.target = target;
+    params.json = json;
+    params.pixelData = pixelData;
+    params.flags = flags;
+
+    return context->InvokeService(context, _OrthancPluginService_CreateDicom, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginDecodeImageCallback callback;
+  } _OrthancPluginDecodeImageCallback;
+
+  /**
+   * @brief Register a callback to handle the decoding of DICOM images.
+   *
+   * This function registers a custom callback to the decoding of
+   * DICOM images, replacing the built-in decoder of Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDecodeImageCallback(
+    OrthancPluginContext*             context,
+    OrthancPluginDecodeImageCallback  callback)
+  {
+    _OrthancPluginDecodeImageCallback params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterDecodeImageCallback, &params);
+  }
+  
+
+
+  typedef struct
+  {
+    OrthancPluginImage**       target;
+    OrthancPluginPixelFormat   format;
+    uint32_t                   width;
+    uint32_t                   height;
+    uint32_t                   pitch;
+    void*                      buffer;
+    const void*                constBuffer;
+    uint32_t                   bufferSize;
+    uint32_t                   frameIndex;
+  } _OrthancPluginCreateImage;
+
+
+  /**
+   * @brief Create an image.
+   *
+   * This function creates an image of given size and format.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param format The format of the pixels.
+   * @param width The width of the image.
+   * @param height The height of the image.
+   * @return The newly allocated image. It must be freed with OrthancPluginFreeImage().
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginCreateImage(
+    OrthancPluginContext*     context,
+    OrthancPluginPixelFormat  format,
+    uint32_t                  width,
+    uint32_t                  height)
+  {
+    OrthancPluginImage* target = NULL;
+
+    _OrthancPluginCreateImage params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.format = format;
+    params.width = width;
+    params.height = height;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateImage, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  /**
+   * @brief Create an image pointing to a memory buffer.
+   *
+   * This function creates an image whose content points to a memory
+   * buffer managed by the plugin. Note that the buffer is directly
+   * accessed, no memory is allocated and no data is copied.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param format The format of the pixels.
+   * @param width The width of the image.
+   * @param height The height of the image.
+   * @param pitch The pitch of the image (i.e. the number of bytes
+   * between 2 successive lines of the image in the memory buffer).
+   * @param buffer The memory buffer.
+   * @return The newly allocated image. It must be freed with OrthancPluginFreeImage().
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginCreateImageAccessor(
+    OrthancPluginContext*     context,
+    OrthancPluginPixelFormat  format,
+    uint32_t                  width,
+    uint32_t                  height,
+    uint32_t                  pitch,
+    void*                     buffer)
+  {
+    OrthancPluginImage* target = NULL;
+
+    _OrthancPluginCreateImage params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.format = format;
+    params.width = width;
+    params.height = height;
+    params.pitch = pitch;
+    params.buffer = buffer;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateImageAccessor, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  /**
+   * @brief Decode one frame from a DICOM instance.
+   *
+   * This function decodes one frame of a DICOM image that is stored
+   * in a memory buffer. This function will give the same result as
+   * OrthancPluginUncompressImage() for single-frame DICOM images.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer Pointer to a memory buffer containing the DICOM image.
+   * @param bufferSize Size of the memory buffer containing the DICOM image.
+   * @param frameIndex The index of the frame of interest in a multi-frame image.
+   * @return The uncompressed image. It must be freed with OrthancPluginFreeImage().
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginDecodeDicomImage(
+    OrthancPluginContext*  context,
+    const void*            buffer,
+    uint32_t               bufferSize,
+    uint32_t               frameIndex)
+  {
+    OrthancPluginImage* target = NULL;
+
+    _OrthancPluginCreateImage params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.constBuffer = buffer;
+    params.bufferSize = bufferSize;
+    params.frameIndex = frameIndex;
+
+    if (context->InvokeService(context, _OrthancPluginService_DecodeDicomImage, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    char**       result;
+    const void*  buffer;
+    uint32_t     size;
+  } _OrthancPluginComputeHash;
+
+  /**
+   * @brief Compute an MD5 hash.
+   *
+   * This functions computes the MD5 cryptographic hash of the given memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The source memory buffer.
+   * @param size The size in bytes of the source buffer.
+   * @return The NULL value in case of error, or a string containing the cryptographic hash.
+   * This string must be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginComputeMd5(
+    OrthancPluginContext*  context,
+    const void*            buffer,
+    uint32_t               size)
+  {
+    char* result;
+
+    _OrthancPluginComputeHash params;
+    params.result = &result;
+    params.buffer = buffer;
+    params.size = size;
+
+    if (context->InvokeService(context, _OrthancPluginService_ComputeMd5, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Compute a SHA-1 hash.
+   *
+   * This functions computes the SHA-1 cryptographic hash of the given memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The source memory buffer.
+   * @param size The size in bytes of the source buffer.
+   * @return The NULL value in case of error, or a string containing the cryptographic hash.
+   * This string must be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginComputeSha1(
+    OrthancPluginContext*  context,
+    const void*            buffer,
+    uint32_t               size)
+  {
+    char* result;
+
+    _OrthancPluginComputeHash params;
+    params.result = &result;
+    params.buffer = buffer;
+    params.size = size;
+
+    if (context->InvokeService(context, _OrthancPluginService_ComputeSha1, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginDictionaryEntry* target;
+    const char*                   name;
+  } _OrthancPluginLookupDictionary;
+
+  /**
+   * @brief Get information about the given DICOM tag.
+   *
+   * This functions makes a lookup in the dictionary of DICOM tags
+   * that are known to Orthanc, and returns information about this
+   * tag. The tag can be specified using its human-readable name
+   * (e.g. "PatientName") or a set of two hexadecimal numbers
+   * (e.g. "0010-0020").
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target Where to store the information about the tag.
+   * @param name The name of the DICOM tag.
+   * @return 0 if success, other value if error.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginLookupDictionary(
+    OrthancPluginContext*          context,
+    OrthancPluginDictionaryEntry*  target,
+    const char*                    name)
+  {
+    _OrthancPluginLookupDictionary params;
+    params.target = target;
+    params.name = name;
+    return context->InvokeService(context, _OrthancPluginService_LookupDictionary, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              answer;
+    uint32_t                 answerSize;
+    uint32_t                 headersCount;
+    const char* const*       headersKeys;
+    const char* const*       headersValues;
+  } _OrthancPluginSendMultipartItem2;
+
+  /**
+   * @brief Send an item as a part of some HTTP multipart answer, with custom headers.
+   *
+   * This function sends an item as a part of some HTTP multipart
+   * answer that was initiated by OrthancPluginStartMultipartAnswer(). In addition to
+   * OrthancPluginSendMultipartItem(), this function will set HTTP header associated
+   * with the item.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param answer Pointer to the memory buffer containing the item.
+   * @param answerSize Number of bytes of the item.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys Array containing the keys of the HTTP headers.
+   * @param headersValues Array containing the values of the HTTP headers.
+   * @return 0 if success, or the error code if failure (this notably happens
+   * if the connection is closed by the client).
+   * @see OrthancPluginSendMultipartItem()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendMultipartItem2(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              answer,
+    uint32_t                 answerSize,
+    uint32_t                 headersCount,
+    const char* const*       headersKeys,
+    const char* const*       headersValues)
+  {
+    _OrthancPluginSendMultipartItem2 params;
+    params.output = output;
+    params.answer = answer;
+    params.answerSize = answerSize;
+    params.headersCount = headersCount;
+    params.headersKeys = headersKeys;
+    params.headersValues = headersValues;    
+
+    return context->InvokeService(context, _OrthancPluginService_SendMultipartItem2, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginIncomingHttpRequestFilter callback;
+  } _OrthancPluginIncomingHttpRequestFilter;
+
+  /**
+   * @brief Register a callback to filter incoming HTTP requests.
+   *
+   * This function registers a custom callback to filter incoming HTTP/REST
+   * requests received by the HTTP server of Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   * @deprecated Please instead use OrthancPluginRegisterIncomingHttpRequestFilter2()
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingHttpRequestFilter(
+    OrthancPluginContext*                   context,
+    OrthancPluginIncomingHttpRequestFilter  callback)
+  {
+    _OrthancPluginIncomingHttpRequestFilter params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterIncomingHttpRequestFilter, &params);
+  }
+  
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  answerBody;
+    OrthancPluginMemoryBuffer*  answerHeaders;
+    uint16_t*                   httpStatus;
+    OrthancPluginHttpMethod     method;
+    const char*                 url;
+    uint32_t                    headersCount;
+    const char* const*          headersKeys;
+    const char* const*          headersValues;
+    const char*                 body;
+    uint32_t                    bodySize;
+    const char*                 username;
+    const char*                 password;
+    uint32_t                    timeout;
+    const char*                 certificateFile;
+    const char*                 certificateKeyFile;
+    const char*                 certificateKeyPassword;
+    uint8_t                     pkcs11;
+  } _OrthancPluginCallHttpClient2;
+
+
+
+  /**
+   * @brief Issue a HTTP call with full flexibility.
+   * 
+   * Make a HTTP call to the given URL. The result to the query is
+   * stored into a newly allocated memory buffer. The HTTP request
+   * will be done accordingly to the global configuration of Orthanc
+   * (in particular, the options "HttpProxy", "HttpTimeout",
+   * "HttpsVerifyPeers", "HttpsCACertificates", and "Pkcs11" will be
+   * taken into account).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answerBody The target memory buffer (out argument).
+   *        It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param answerHeaders The target memory buffer for the HTTP headers in the answers (out argument). 
+   *        The answer headers are formatted as a JSON object (associative array).
+   *        The buffer must be freed with OrthancPluginFreeMemoryBuffer().
+   *        This argument can be set to NULL if the plugin has no interest in the HTTP headers.
+   * @param httpStatus The HTTP status after the execution of the request (out argument).
+   * @param method HTTP method to be used.
+   * @param url The URL of interest.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys Array containing the keys of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param headersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param username The username (can be <tt>NULL</tt> if no password protection).
+   * @param password The password (can be <tt>NULL</tt> if no password protection).
+   * @param body The HTTP body for a POST or PUT request.
+   * @param bodySize The size of the body.
+   * @param timeout Timeout in seconds (0 for default timeout).
+   * @param certificateFile Path to the client certificate for HTTPS, in PEM format
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param certificateKeyFile Path to the key of the client certificate for HTTPS, in PEM format
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param certificateKeyPassword Password to unlock the key of the client certificate 
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param pkcs11 Enable PKCS#11 client authentication for hardware security modules and smart cards.
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginCallPeerApi()
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpClient(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  answerBody,
+    OrthancPluginMemoryBuffer*  answerHeaders,
+    uint16_t*                   httpStatus,
+    OrthancPluginHttpMethod     method,
+    const char*                 url,
+    uint32_t                    headersCount,
+    const char* const*          headersKeys,
+    const char* const*          headersValues,
+    const char*                 body,
+    uint32_t                    bodySize,
+    const char*                 username,
+    const char*                 password,
+    uint32_t                    timeout,
+    const char*                 certificateFile,
+    const char*                 certificateKeyFile,
+    const char*                 certificateKeyPassword,
+    uint8_t                     pkcs11)
+  {
+    _OrthancPluginCallHttpClient2 params;
+    memset(&params, 0, sizeof(params));
+
+    params.answerBody = answerBody;
+    params.answerHeaders = answerHeaders;
+    params.httpStatus = httpStatus;
+    params.method = method;
+    params.url = url;
+    params.headersCount = headersCount;
+    params.headersKeys = headersKeys;
+    params.headersValues = headersValues;
+    params.body = body;
+    params.bodySize = bodySize;
+    params.username = username;
+    params.password = password;
+    params.timeout = timeout;
+    params.certificateFile = certificateFile;
+    params.certificateKeyFile = certificateKeyFile;
+    params.certificateKeyPassword = certificateKeyPassword;
+    params.pkcs11 = pkcs11;
+
+    return context->InvokeService(context, _OrthancPluginService_CallHttpClient2, &params);
+  }
+
+
+  /**
+   * @brief Generate an UUID.
+   *
+   * Generate a random GUID/UUID (globally unique identifier).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return NULL in the case of an error, or a newly allocated string
+   * containing the UUID. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGenerateUuid(
+    OrthancPluginContext*  context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GenerateUuid, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginFindCallback callback;
+  } _OrthancPluginFindCallback;
+
+  /**
+   * @brief Register a callback to handle C-Find requests.
+   *
+   * This function registers a callback to handle C-Find SCP requests
+   * that are not related to modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterFindCallback(
+    OrthancPluginContext*      context,
+    OrthancPluginFindCallback  callback)
+  {
+    _OrthancPluginFindCallback params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterFindCallback, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginFindAnswers      *answers;
+    const OrthancPluginFindQuery  *query;
+    const void                    *dicom;
+    uint32_t                       size;
+    uint32_t                       index;
+    uint32_t                      *resultUint32;
+    uint16_t                      *resultGroup;
+    uint16_t                      *resultElement;
+    char                         **resultString;
+  } _OrthancPluginFindOperation;
+
+  /**
+   * @brief Add one answer to some C-Find request.
+   *
+   * This function adds one answer (encoded as a DICOM file) to the
+   * set of answers corresponding to some C-Find SCP request that is
+   * not related to modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answers The set of answers.
+   * @param dicom The answer to be added, encoded as a DICOM file.
+   * @param size The size of the DICOM file.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   * @see OrthancPluginCreateDicom()
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginFindAddAnswer(
+    OrthancPluginContext*      context,
+    OrthancPluginFindAnswers*  answers,
+    const void*                dicom,
+    uint32_t                   size)
+  {
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.answers = answers;
+    params.dicom = dicom;
+    params.size = size;
+
+    return context->InvokeService(context, _OrthancPluginService_FindAddAnswer, &params);
+  }
+
+
+  /**
+   * @brief Mark the set of C-Find answers as incomplete.
+   *
+   * This function marks as incomplete the set of answers
+   * corresponding to some C-Find SCP request that is not related to
+   * modality worklists. This must be used if canceling the handling
+   * of a request when too many answers are to be returned.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answers The set of answers.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginFindMarkIncomplete(
+    OrthancPluginContext*      context,
+    OrthancPluginFindAnswers*  answers)
+  {
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.answers = answers;
+
+    return context->InvokeService(context, _OrthancPluginService_FindMarkIncomplete, &params);
+  }
+
+
+
+  /**
+   * @brief Get the number of tags in a C-Find query.
+   *
+   * This function returns the number of tags that are contained in
+   * the given C-Find query.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The C-Find query.
+   * @return The number of tags.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t  OrthancPluginGetFindQuerySize(
+    OrthancPluginContext*          context,
+    const OrthancPluginFindQuery*  query)
+  {
+    uint32_t count = 0;
+
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.query = query;
+    params.resultUint32 = &count;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFindQuerySize, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return count;
+    }
+  }
+
+
+  /**
+   * @brief Get one tag in a C-Find query.
+   *
+   * This function returns the group and the element of one DICOM tag
+   * in the given C-Find query.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param group The group of the tag (output).
+   * @param element The element of the tag (output).
+   * @param query The C-Find query.
+   * @param index The index of the tag of interest.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginGetFindQueryTag(
+    OrthancPluginContext*          context,
+    uint16_t*                      group,
+    uint16_t*                      element,
+    const OrthancPluginFindQuery*  query,
+    uint32_t                       index)
+  {
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.query = query;
+    params.index = index;
+    params.resultGroup = group;
+    params.resultElement = element;
+
+    return context->InvokeService(context, _OrthancPluginService_GetFindQueryTag, &params);
+  }
+
+
+  /**
+   * @brief Get the symbolic name of one tag in a C-Find query.
+   *
+   * This function returns the symbolic name of one DICOM tag in the
+   * given C-Find query.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The C-Find query.
+   * @param index The index of the tag of interest.
+   * @return The NULL value in case of error, or a string containing the name of the tag.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE char*  OrthancPluginGetFindQueryTagName(
+    OrthancPluginContext*          context,
+    const OrthancPluginFindQuery*  query,
+    uint32_t                       index)
+  {
+    char* result;
+
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.query = query;
+    params.index = index;
+    params.resultString = &result;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFindQueryTagName, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the value associated with one tag in a C-Find query.
+   *
+   * This function returns the value associated with one tag in the
+   * given C-Find query.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The C-Find query.
+   * @param index The index of the tag of interest.
+   * @return The NULL value in case of error, or a string containing the value of the tag.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE char*  OrthancPluginGetFindQueryValue(
+    OrthancPluginContext*          context,
+    const OrthancPluginFindQuery*  query,
+    uint32_t                       index)
+  {
+    char* result;
+
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.query = query;
+    params.index = index;
+    params.resultString = &result;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFindQueryValue, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+ 
+
+
+
+  typedef struct
+  {
+    OrthancPluginMoveCallback   callback;
+    OrthancPluginGetMoveSize    getMoveSize;
+    OrthancPluginApplyMove      applyMove;
+    OrthancPluginFreeMove       freeMove;
+  } _OrthancPluginMoveCallback;
+
+  /**
+   * @brief Register a callback to handle C-Move requests.
+   *
+   * This function registers a callback to handle C-Move SCP requests.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The main callback.
+   * @param getMoveSize Callback to read the number of C-Move suboperations.
+   * @param applyMove Callback to apply one C-Move suboperations.
+   * @param freeMove Callback to free the C-Move driver.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterMoveCallback(
+    OrthancPluginContext*       context,
+    OrthancPluginMoveCallback   callback,
+    OrthancPluginGetMoveSize    getMoveSize,
+    OrthancPluginApplyMove      applyMove,
+    OrthancPluginFreeMove       freeMove)
+  {
+    _OrthancPluginMoveCallback params;
+    params.callback = callback;
+    params.getMoveSize = getMoveSize;
+    params.applyMove = applyMove;
+    params.freeMove = freeMove;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterMoveCallback, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginFindMatcher** target;
+    const void*                query;
+    uint32_t                   size;
+  } _OrthancPluginCreateFindMatcher;
+
+
+  /**
+   * @brief Create a C-Find matcher.
+   *
+   * This function creates a "matcher" object that can be used to
+   * check whether a DICOM instance matches a C-Find query. The C-Find
+   * query must be expressed as a DICOM buffer.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The C-Find DICOM query.
+   * @param size The size of the DICOM query.
+   * @return The newly allocated matcher. It must be freed with OrthancPluginFreeFindMatcher().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginFindMatcher* OrthancPluginCreateFindMatcher(
+    OrthancPluginContext*  context,
+    const void*            query,
+    uint32_t               size)
+  {
+    OrthancPluginFindMatcher* target = NULL;
+
+    _OrthancPluginCreateFindMatcher params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.query = query;
+    params.size = size;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateFindMatcher, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginFindMatcher*   matcher;
+  } _OrthancPluginFreeFindMatcher;
+
+  /**
+   * @brief Free a C-Find matcher.
+   *
+   * This function frees a matcher that was created using OrthancPluginCreateFindMatcher().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param matcher The matcher of interest.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeFindMatcher(
+    OrthancPluginContext*     context, 
+    OrthancPluginFindMatcher* matcher)
+  {
+    _OrthancPluginFreeFindMatcher params;
+    params.matcher = matcher;
+
+    context->InvokeService(context, _OrthancPluginService_FreeFindMatcher, &params);
+  }
+
+
+  typedef struct
+  {
+    const OrthancPluginFindMatcher*  matcher;
+    const void*                      dicom;
+    uint32_t                         size;
+    int32_t*                         isMatch;
+  } _OrthancPluginFindMatcherIsMatch;
+
+  /**
+   * @brief Test whether a DICOM instance matches a C-Find query.
+   *
+   * This function checks whether one DICOM instance matches C-Find
+   * matcher that was previously allocated using
+   * OrthancPluginCreateFindMatcher().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param matcher The matcher of interest.
+   * @param dicom The DICOM instance to be matched.
+   * @param size The size of the DICOM instance.
+   * @return 1 if the DICOM instance matches the query, 0 otherwise.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginFindMatcherIsMatch(
+    OrthancPluginContext*            context,
+    const OrthancPluginFindMatcher*  matcher,
+    const void*                      dicom,
+    uint32_t                         size)
+  {
+    int32_t isMatch = 0;
+
+    _OrthancPluginFindMatcherIsMatch params;
+    params.matcher = matcher;
+    params.dicom = dicom;
+    params.size = size;
+    params.isMatch = &isMatch;
+
+    if (context->InvokeService(context, _OrthancPluginService_FindMatcherIsMatch, &params) == OrthancPluginErrorCode_Success)
+    {
+      return isMatch;
+    }
+    else
+    {
+      /* Error: Assume non-match */
+      return 0;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginIncomingHttpRequestFilter2 callback;
+  } _OrthancPluginIncomingHttpRequestFilter2;
+
+  /**
+   * @brief Register a callback to filter incoming HTTP requests.
+   *
+   * This function registers a custom callback to filter incoming HTTP/REST
+   * requests received by the HTTP server of Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingHttpRequestFilter2(
+    OrthancPluginContext*                   context,
+    OrthancPluginIncomingHttpRequestFilter2 callback)
+  {
+    _OrthancPluginIncomingHttpRequestFilter2 params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterIncomingHttpRequestFilter2, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginPeers**  peers;
+  } _OrthancPluginGetPeers;
+
+  /**
+   * @brief Return the list of available Orthanc peers.
+   *
+   * This function returns the parameters of the Orthanc peers that are known to
+   * the Orthanc server hosting the plugin.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return NULL if error, or a newly allocated opaque data structure containing the peers.
+   * This structure must be freed with OrthancPluginFreePeers().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginPeers* OrthancPluginGetPeers(
+    OrthancPluginContext*  context)
+  {
+    OrthancPluginPeers* peers = NULL;
+
+    _OrthancPluginGetPeers params;
+    memset(&params, 0, sizeof(params));
+    params.peers = &peers;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeers, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return peers;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginPeers*   peers;
+  } _OrthancPluginFreePeers;
+
+  /**
+   * @brief Free the list of available Orthanc peers.
+   *
+   * This function frees the data structure returned by OrthancPluginGetPeers().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreePeers(
+    OrthancPluginContext*     context, 
+    OrthancPluginPeers* peers)
+  {
+    _OrthancPluginFreePeers params;
+    params.peers = peers;
+
+    context->InvokeService(context, _OrthancPluginService_FreePeers, &params);
+  }
+
+
+  typedef struct
+  {
+    uint32_t*                  target;
+    const OrthancPluginPeers*  peers;
+  } _OrthancPluginGetPeersCount;
+
+  /**
+   * @brief Get the number of Orthanc peers.
+   *
+   * This function returns the number of Orthanc peers.
+   *
+   * This function is thread-safe: Several threads sharing the same
+   * OrthancPluginPeers object can simultaneously call this function.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @result The number of peers. 
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetPeersCount(
+    OrthancPluginContext*      context,
+    const OrthancPluginPeers*  peers)
+  {
+    uint32_t target = 0;
+
+    _OrthancPluginGetPeersCount params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.peers = peers;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeersCount, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    const char**               target;
+    const OrthancPluginPeers*  peers;
+    uint32_t                   peerIndex;
+    const char*                userProperty;
+  } _OrthancPluginGetPeerProperty;
+
+  /**
+   * @brief Get the symbolic name of an Orthanc peer.
+   *
+   * This function returns the symbolic name of the Orthanc peer,
+   * which corresponds to the key of the "OrthancPeers" configuration
+   * option of Orthanc.
+   *
+   * This function is thread-safe: Several threads sharing the same
+   * OrthancPluginPeers object can simultaneously call this function.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @param peerIndex The index of the peer of interest.
+   * This value must be lower than OrthancPluginGetPeersCount().
+   * @result The symbolic name, or NULL in the case of an error.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerName(
+    OrthancPluginContext*      context,
+    const OrthancPluginPeers*  peers,
+    uint32_t                   peerIndex)
+  {
+    const char* target = NULL;
+
+    _OrthancPluginGetPeerProperty params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.peers = peers;
+    params.peerIndex = peerIndex;
+    params.userProperty = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeerName, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  /**
+   * @brief Get the base URL of an Orthanc peer.
+   *
+   * This function returns the base URL to the REST API of some Orthanc peer.
+   *
+   * This function is thread-safe: Several threads sharing the same
+   * OrthancPluginPeers object can simultaneously call this function.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @param peerIndex The index of the peer of interest.
+   * This value must be lower than OrthancPluginGetPeersCount().
+   * @result The URL, or NULL in the case of an error.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerUrl(
+    OrthancPluginContext*      context,
+    const OrthancPluginPeers*  peers,
+    uint32_t                   peerIndex)
+  {
+    const char* target = NULL;
+
+    _OrthancPluginGetPeerProperty params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.peers = peers;
+    params.peerIndex = peerIndex;
+    params.userProperty = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeerUrl, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  /**
+   * @brief Get some user-defined property of an Orthanc peer.
+   *
+   * This function returns some user-defined property of some Orthanc
+   * peer. An user-defined property is a property that is associated
+   * with the peer in the Orthanc configuration file, but that is not
+   * recognized by the Orthanc core.
+   *
+   * This function is thread-safe: Several threads sharing the same
+   * OrthancPluginPeers object can simultaneously call this function.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @param peerIndex The index of the peer of interest.
+   * This value must be lower than OrthancPluginGetPeersCount().
+   * @param userProperty The user property of interest.
+   * @result The value of the user property, or NULL if it is not defined.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerUserProperty(
+    OrthancPluginContext*      context,
+    const OrthancPluginPeers*  peers,
+    uint32_t                   peerIndex,
+    const char*                userProperty)
+  {
+    const char* target = NULL;
+
+    _OrthancPluginGetPeerProperty params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.peers = peers;
+    params.peerIndex = peerIndex;
+    params.userProperty = userProperty;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeerUserProperty, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* No such user property */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  answerBody;
+    OrthancPluginMemoryBuffer*  answerHeaders;
+    uint16_t*                   httpStatus;
+    const OrthancPluginPeers*   peers;
+    uint32_t                    peerIndex;
+    OrthancPluginHttpMethod     method;
+    const char*                 uri;
+    uint32_t                    additionalHeadersCount;
+    const char* const*          additionalHeadersKeys;
+    const char* const*          additionalHeadersValues;
+    const char*                 body;
+    uint32_t                    bodySize;
+    uint32_t                    timeout;
+  } _OrthancPluginCallPeerApi;
+
+  /**
+   * @brief Call the REST API of an Orthanc peer.
+   * 
+   * Make a REST call to the given URI in the REST API of a remote
+   * Orthanc peer. The result to the query is stored into a newly
+   * allocated memory buffer. The HTTP request will be done according
+   * to the "OrthancPeers" configuration option of Orthanc.
+   *
+   * This function is thread-safe: Several threads sharing the same
+   * OrthancPluginPeers object can simultaneously call this function.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answerBody The target memory buffer (out argument).
+   *        It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param answerHeaders The target memory buffer for the HTTP headers in the answers (out argument). 
+   *        The answer headers are formatted as a JSON object (associative array).
+   *        The buffer must be freed with OrthancPluginFreeMemoryBuffer().
+   *        This argument can be set to NULL if the plugin has no interest in the HTTP headers.
+   * @param httpStatus The HTTP status after the execution of the request (out argument).
+   * @param peers The data structure describing the Orthanc peers.
+   * @param peerIndex The index of the peer of interest.
+   * This value must be lower than OrthancPluginGetPeersCount().
+   * @param method HTTP method to be used.
+   * @param uri The URI of interest in the REST API.
+   * @param additionalHeadersCount The number of HTTP headers to be added to the
+   * HTTP headers provided in the global configuration of Orthanc.
+   * @param additionalHeadersKeys Array containing the keys of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param additionalHeadersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param body The HTTP body for a POST or PUT request.
+   * @param bodySize The size of the body.
+   * @param timeout Timeout in seconds (0 for default timeout).
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginHttpClient()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginCallPeerApi(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  answerBody,
+    OrthancPluginMemoryBuffer*  answerHeaders,
+    uint16_t*                   httpStatus,
+    const OrthancPluginPeers*   peers,
+    uint32_t                    peerIndex,
+    OrthancPluginHttpMethod     method,
+    const char*                 uri,
+    uint32_t                    additionalHeadersCount,
+    const char* const*          additionalHeadersKeys,
+    const char* const*          additionalHeadersValues,
+    const char*                 body,
+    uint32_t                    bodySize,
+    uint32_t                    timeout)
+  {
+    _OrthancPluginCallPeerApi params;
+    memset(&params, 0, sizeof(params));
+
+    params.answerBody = answerBody;
+    params.answerHeaders = answerHeaders;
+    params.httpStatus = httpStatus;
+    params.peers = peers;
+    params.peerIndex = peerIndex;
+    params.method = method;
+    params.uri = uri;
+    params.additionalHeadersCount = additionalHeadersCount;
+    params.additionalHeadersKeys = additionalHeadersKeys;
+    params.additionalHeadersValues = additionalHeadersValues;
+    params.body = body;
+    params.bodySize = bodySize;
+    params.timeout = timeout;
+
+    return context->InvokeService(context, _OrthancPluginService_CallPeerApi, &params);
+  }
+
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginJob**              target;
+    void                           *job;
+    OrthancPluginJobFinalize        finalize;
+    const char                     *type;
+    OrthancPluginJobGetProgress     getProgress;
+    OrthancPluginJobGetContent      getContent;
+    OrthancPluginJobGetSerialized   getSerialized;
+    OrthancPluginJobStep            step;
+    OrthancPluginJobStop            stop;
+    OrthancPluginJobReset           reset;
+  } _OrthancPluginCreateJob;
+
+  /**
+   * @brief Create a custom job.
+   *
+   * This function creates a custom job to be run by the jobs engine
+   * of Orthanc.
+   * 
+   * Orthanc starts one dedicated thread per custom job that is
+   * running. It is guaranteed that all the callbacks will only be
+   * called from this single dedicated thread, in mutual exclusion: As
+   * a consequence, it is *not* mandatory to protect the various
+   * callbacks by mutexes.
+   * 
+   * The custom job can nonetheless launch its own processing threads
+   * on the first call to the "step()" callback, and stop them once
+   * the "stop()" callback is called.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param job The job to be executed.
+   * @param finalize The finalization callback.
+   * @param type The type of the job, provided to the job unserializer. 
+   * See OrthancPluginRegisterJobsUnserializer().
+   * @param getProgress The progress callback.
+   * @param getContent The content callback.
+   * @param getSerialized The serialization callback.
+   * @param step The callback to execute the individual steps of the job.
+   * @param stop The callback that is invoked once the job leaves the "running" state.
+   * @param reset The callback that is invoked if a stopped job is started again.
+   * @return The newly allocated job. It must be freed with OrthancPluginFreeJob(),
+   * as long as it is not submitted with OrthancPluginSubmitJob().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginJob *OrthancPluginCreateJob(
+    OrthancPluginContext           *context,
+    void                           *job,
+    OrthancPluginJobFinalize        finalize,
+    const char                     *type,
+    OrthancPluginJobGetProgress     getProgress,
+    OrthancPluginJobGetContent      getContent,
+    OrthancPluginJobGetSerialized   getSerialized,
+    OrthancPluginJobStep            step,
+    OrthancPluginJobStop            stop,
+    OrthancPluginJobReset           reset)
+  {
+    OrthancPluginJob* target = NULL;
+
+    _OrthancPluginCreateJob params;
+    memset(&params, 0, sizeof(params));
+
+    params.target = &target;
+    params.job = job;
+    params.finalize = finalize;
+    params.type = type;
+    params.getProgress = getProgress;
+    params.getContent = getContent;
+    params.getSerialized = getSerialized;
+    params.step = step;
+    params.stop = stop;
+    params.reset = reset;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateJob, &params) != OrthancPluginErrorCode_Success ||
+        target == NULL)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginJob*   job;
+  } _OrthancPluginFreeJob;
+
+  /**
+   * @brief Free a custom job.
+   *
+   * This function frees an image that was created with OrthancPluginCreateJob().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param job The job.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeJob(
+    OrthancPluginContext* context, 
+    OrthancPluginJob*     job)
+  {
+    _OrthancPluginFreeJob params;
+    params.job = job;
+
+    context->InvokeService(context, _OrthancPluginService_FreeJob, &params);
+  }
+
+
+  
+  typedef struct
+  {
+    char**             resultId;
+    OrthancPluginJob  *job;
+    int                priority;
+  } _OrthancPluginSubmitJob;
+
+  /**
+   * @brief Submit a new job to the jobs engine of Orthanc.
+   *
+   * This function adds the given job to the pending jobs of
+   * Orthanc. Orthanc will take take of freeing it by invoking the
+   * finalization callback provided to OrthancPluginCreateJob().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param job The job, as received by OrthancPluginCreateJob().
+   * @param priority The priority of the job.
+   * @return ID of the newly-submitted job. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char *OrthancPluginSubmitJob(
+    OrthancPluginContext   *context,
+    OrthancPluginJob       *job,
+    int                     priority)
+  {
+    char* resultId = NULL;
+
+    _OrthancPluginSubmitJob params;
+    memset(&params, 0, sizeof(params));
+
+    params.resultId = &resultId;
+    params.job = job;
+    params.priority = priority;
+
+    if (context->InvokeService(context, _OrthancPluginService_SubmitJob, &params) != OrthancPluginErrorCode_Success ||
+        resultId == NULL)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return resultId;
+    }
+  }
+  
+
+
+  typedef struct
+  {
+    OrthancPluginJobsUnserializer unserializer;
+  } _OrthancPluginJobsUnserializer;
+
+  /**
+   * @brief Register an unserializer for custom jobs.
+   *
+   * This function registers an unserializer that decodes custom jobs
+   * from a JSON string. This callback is invoked when the jobs engine
+   * of Orthanc is started (on Orthanc initialization), for each job
+   * that is stored in the Orthanc database.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param unserializer The job unserializer.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterJobsUnserializer(
+    OrthancPluginContext*          context,
+    OrthancPluginJobsUnserializer  unserializer)
+  {
+    _OrthancPluginJobsUnserializer params;
+    params.unserializer = unserializer;
+
+    context->InvokeService(context, _OrthancPluginService_RegisterJobsUnserializer, &params);
+  }
+
+
 #ifdef  __cplusplus
 }
 #endif
--- a/Plugins/Include/orthanc/OrthancCppDatabasePlugin.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1895 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-
-#pragma once
-
-#include "OrthancCDatabasePlugin.h"
-
-#include <stdexcept>
-#include <list>
-#include <string>
-
-namespace OrthancPlugins
-{
-//! @cond Doxygen_Suppress
-  // This class mimics "boost::noncopyable"
-  class NonCopyable
-  {
-  private:
-    NonCopyable(const NonCopyable&);
-
-    NonCopyable& operator= (const NonCopyable&);
-
-  protected:
-    NonCopyable()
-    {
-    }
-
-    ~NonCopyable()
-    {
-    }
-  };
-//! @endcond
-
-
-  /**
-   * @ingroup Callbacks
-   **/
-  class DatabaseException
-  {
-  private:
-    OrthancPluginErrorCode  code_;
-
-  public:
-    DatabaseException() : code_(OrthancPluginErrorCode_DatabasePlugin)
-    {
-    }
-
-    DatabaseException(OrthancPluginErrorCode code) : code_(code)
-    {
-    }
-
-    OrthancPluginErrorCode  GetErrorCode() const
-    {
-      return code_;
-    }
-  };
-
-
-  /**
-   * @ingroup Callbacks
-   **/
-  class DatabaseBackendOutput : public NonCopyable
-  {
-    friend class DatabaseBackendAdapter;
-
-  private:
-    enum AllowedAnswers
-    {
-      AllowedAnswers_All,
-      AllowedAnswers_None,
-      AllowedAnswers_Attachment,
-      AllowedAnswers_Change,
-      AllowedAnswers_DicomTag,
-      AllowedAnswers_ExportedResource
-    };
-
-    OrthancPluginContext*         context_;
-    OrthancPluginDatabaseContext* database_;
-    AllowedAnswers                allowedAnswers_;
-
-    void SetAllowedAnswers(AllowedAnswers allowed)
-    {
-      allowedAnswers_ = allowed;
-    }
-
-  public:
-    DatabaseBackendOutput(OrthancPluginContext*         context,
-                          OrthancPluginDatabaseContext* database) :
-      context_(context),
-      database_(database),
-      allowedAnswers_(AllowedAnswers_All /* for unit tests */)
-    {
-    }
-
-    OrthancPluginContext* GetContext()
-    {
-      return context_;
-    }
-
-    void LogError(const std::string& message)
-    {
-      OrthancPluginLogError(context_, message.c_str());
-    }
-
-    void LogWarning(const std::string& message)
-    {
-      OrthancPluginLogWarning(context_, message.c_str());
-    }
-
-    void LogInfo(const std::string& message)
-    {
-      OrthancPluginLogInfo(context_, message.c_str());
-    }
-
-    void SignalDeletedAttachment(const std::string& uuid,
-                                 int32_t            contentType,
-                                 uint64_t           uncompressedSize,
-                                 const std::string& uncompressedHash,
-                                 int32_t            compressionType,
-                                 uint64_t           compressedSize,
-                                 const std::string& compressedHash)
-    {
-      OrthancPluginAttachment attachment;
-      attachment.uuid = uuid.c_str();
-      attachment.contentType = contentType;
-      attachment.uncompressedSize = uncompressedSize;
-      attachment.uncompressedHash = uncompressedHash.c_str();
-      attachment.compressionType = compressionType;
-      attachment.compressedSize = compressedSize;
-      attachment.compressedHash = compressedHash.c_str();
-
-      OrthancPluginDatabaseSignalDeletedAttachment(context_, database_, &attachment);
-    }
-
-    void SignalDeletedResource(const std::string& publicId,
-                               OrthancPluginResourceType resourceType)
-    {
-      OrthancPluginDatabaseSignalDeletedResource(context_, database_, publicId.c_str(), resourceType);
-    }
-
-    void SignalRemainingAncestor(const std::string& ancestorId,
-                                 OrthancPluginResourceType ancestorType)
-    {
-      OrthancPluginDatabaseSignalRemainingAncestor(context_, database_, ancestorId.c_str(), ancestorType);
-    }
-
-    void AnswerAttachment(const std::string& uuid,
-                          int32_t            contentType,
-                          uint64_t           uncompressedSize,
-                          const std::string& uncompressedHash,
-                          int32_t            compressionType,
-                          uint64_t           compressedSize,
-                          const std::string& compressedHash)
-    {
-      if (allowedAnswers_ != AllowedAnswers_All &&
-          allowedAnswers_ != AllowedAnswers_Attachment)
-      {
-        throw std::runtime_error("Cannot answer with an attachment in the current state");
-      }
-
-      OrthancPluginAttachment attachment;
-      attachment.uuid = uuid.c_str();
-      attachment.contentType = contentType;
-      attachment.uncompressedSize = uncompressedSize;
-      attachment.uncompressedHash = uncompressedHash.c_str();
-      attachment.compressionType = compressionType;
-      attachment.compressedSize = compressedSize;
-      attachment.compressedHash = compressedHash.c_str();
-
-      OrthancPluginDatabaseAnswerAttachment(context_, database_, &attachment);
-    }
-
-    void AnswerChange(int64_t                    seq,
-                      int32_t                    changeType,
-                      OrthancPluginResourceType  resourceType,
-                      const std::string&         publicId,
-                      const std::string&         date)
-    {
-      if (allowedAnswers_ != AllowedAnswers_All &&
-          allowedAnswers_ != AllowedAnswers_Change)
-      {
-        throw std::runtime_error("Cannot answer with a change in the current state");
-      }
-
-      OrthancPluginChange change;
-      change.seq = seq;
-      change.changeType = changeType;
-      change.resourceType = resourceType;
-      change.publicId = publicId.c_str();
-      change.date = date.c_str();
-
-      OrthancPluginDatabaseAnswerChange(context_, database_, &change);
-    }
-
-    void AnswerDicomTag(uint16_t group,
-                        uint16_t element,
-                        const std::string& value)
-    {
-      if (allowedAnswers_ != AllowedAnswers_All &&
-          allowedAnswers_ != AllowedAnswers_DicomTag)
-      {
-        throw std::runtime_error("Cannot answer with a DICOM tag in the current state");
-      }
-
-      OrthancPluginDicomTag tag;
-      tag.group = group;
-      tag.element = element;
-      tag.value = value.c_str();
-
-      OrthancPluginDatabaseAnswerDicomTag(context_, database_, &tag);
-    }
-
-    void AnswerExportedResource(int64_t                    seq,
-                                OrthancPluginResourceType  resourceType,
-                                const std::string&         publicId,
-                                const std::string&         modality,
-                                const std::string&         date,
-                                const std::string&         patientId,
-                                const std::string&         studyInstanceUid,
-                                const std::string&         seriesInstanceUid,
-                                const std::string&         sopInstanceUid)
-    {
-      if (allowedAnswers_ != AllowedAnswers_All &&
-          allowedAnswers_ != AllowedAnswers_ExportedResource)
-      {
-        throw std::runtime_error("Cannot answer with an exported resource in the current state");
-      }
-
-      OrthancPluginExportedResource exported;
-      exported.seq = seq;
-      exported.resourceType = resourceType;
-      exported.publicId = publicId.c_str();
-      exported.modality = modality.c_str();
-      exported.date = date.c_str();
-      exported.patientId = patientId.c_str();
-      exported.studyInstanceUid = studyInstanceUid.c_str();
-      exported.seriesInstanceUid = seriesInstanceUid.c_str();
-      exported.sopInstanceUid = sopInstanceUid.c_str();
-
-      OrthancPluginDatabaseAnswerExportedResource(context_, database_, &exported);
-    }
-  };
-
-
-  /**
-   * @ingroup Callbacks
-   **/
-  class IDatabaseBackend : public NonCopyable
-  {
-    friend class DatabaseBackendAdapter;
-
-  private:
-    DatabaseBackendOutput*  output_;
-
-    void Finalize()
-    {
-      if (output_ != NULL)
-      {
-        delete output_;
-        output_ = NULL;
-      }
-    }
-
-  protected:
-    DatabaseBackendOutput& GetOutput()
-    {
-      return *output_;
-    }
-
-  public:
-    IDatabaseBackend() : output_(NULL)
-    {
-    }
-
-    virtual ~IDatabaseBackend()
-    {
-      Finalize();
-    }
-
-    // This takes the ownership
-    void RegisterOutput(DatabaseBackendOutput* output)
-    {
-      Finalize();
-      output_ = output;
-    }
-
-    virtual void Open() = 0;
-
-    virtual void Close() = 0;
-
-    virtual void AddAttachment(int64_t id,
-                               const OrthancPluginAttachment& attachment) = 0;
-
-    virtual void AttachChild(int64_t parent,
-                             int64_t child) = 0;
-
-    virtual void ClearChanges() = 0;
-
-    virtual void ClearExportedResources() = 0;
-
-    virtual int64_t CreateResource(const char* publicId,
-                                   OrthancPluginResourceType type) = 0;
-
-    virtual void DeleteAttachment(int64_t id,
-                                  int32_t attachment) = 0;
-
-    virtual void DeleteMetadata(int64_t id,
-                                int32_t metadataType) = 0;
-
-    virtual void DeleteResource(int64_t id) = 0;
-
-    virtual void GetAllInternalIds(std::list<int64_t>& target,
-                                   OrthancPluginResourceType resourceType) = 0;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 OrthancPluginResourceType resourceType) = 0;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 OrthancPluginResourceType resourceType,
-                                 uint64_t since,
-                                 uint64_t limit) = 0;
-
-    /* Use GetOutput().AnswerChange() */
-    virtual void GetChanges(bool& done /*out*/,
-                            int64_t since,
-                            uint32_t maxResults) = 0;
-
-    virtual void GetChildrenInternalId(std::list<int64_t>& target /*out*/,
-                                       int64_t id) = 0;
-
-    virtual void GetChildrenPublicId(std::list<std::string>& target /*out*/,
-                                     int64_t id) = 0;
-
-    /* Use GetOutput().AnswerExportedResource() */
-    virtual void GetExportedResources(bool& done /*out*/,
-                                      int64_t since,
-                                      uint32_t maxResults) = 0;
-
-    /* Use GetOutput().AnswerChange() */
-    virtual void GetLastChange() = 0;
-
-    /* Use GetOutput().AnswerExportedResource() */
-    virtual void GetLastExportedResource() = 0;
-
-    /* Use GetOutput().AnswerDicomTag() */
-    virtual void GetMainDicomTags(int64_t id) = 0;
-
-    virtual std::string GetPublicId(int64_t resourceId) = 0;
-
-    virtual uint64_t GetResourceCount(OrthancPluginResourceType resourceType) = 0;
-
-    virtual OrthancPluginResourceType GetResourceType(int64_t resourceId) = 0;
-
-    virtual uint64_t GetTotalCompressedSize() = 0;
-    
-    virtual uint64_t GetTotalUncompressedSize() = 0;
-
-    virtual bool IsExistingResource(int64_t internalId) = 0;
-
-    virtual bool IsProtectedPatient(int64_t internalId) = 0;
-
-    virtual void ListAvailableMetadata(std::list<int32_t>& target /*out*/,
-                                       int64_t id) = 0;
-
-    virtual void ListAvailableAttachments(std::list<int32_t>& target /*out*/,
-                                          int64_t id) = 0;
-
-    virtual void LogChange(const OrthancPluginChange& change) = 0;
-
-    virtual void LogExportedResource(const OrthancPluginExportedResource& resource) = 0;
-    
-    /* Use GetOutput().AnswerAttachment() */
-    virtual bool LookupAttachment(int64_t id,
-                                  int32_t contentType) = 0;
-
-    virtual bool LookupGlobalProperty(std::string& target /*out*/,
-                                      int32_t property) = 0;
-
-    virtual void LookupIdentifier(std::list<int64_t>& target /*out*/,
-                                  OrthancPluginResourceType resourceType,
-                                  uint16_t group,
-                                  uint16_t element,
-                                  OrthancPluginIdentifierConstraint constraint,
-                                  const char* value) = 0;
-
-    virtual bool LookupMetadata(std::string& target /*out*/,
-                                int64_t id,
-                                int32_t metadataType) = 0;
-
-    virtual bool LookupParent(int64_t& parentId /*out*/,
-                              int64_t resourceId) = 0;
-
-    virtual bool LookupResource(int64_t& id /*out*/,
-                                OrthancPluginResourceType& type /*out*/,
-                                const char* publicId) = 0;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/) = 0;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/,
-                                        int64_t patientIdToAvoid) = 0;
-
-    virtual void SetGlobalProperty(int32_t property,
-                                   const char* value) = 0;
-
-    virtual void SetMainDicomTag(int64_t id,
-                                 uint16_t group,
-                                 uint16_t element,
-                                 const char* value) = 0;
-
-    virtual void SetIdentifierTag(int64_t id,
-                                  uint16_t group,
-                                  uint16_t element,
-                                  const char* value) = 0;
-
-    virtual void SetMetadata(int64_t id,
-                             int32_t metadataType,
-                             const char* value) = 0;
-
-    virtual void SetProtectedPatient(int64_t internalId, 
-                                     bool isProtected) = 0;
-
-    virtual void StartTransaction() = 0;
-
-    virtual void RollbackTransaction() = 0;
-
-    virtual void CommitTransaction() = 0;
-
-    virtual uint32_t GetDatabaseVersion() = 0;
-
-    /**
-     * Upgrade the database to the specified version of the database
-     * schema.  The upgrade script is allowed to make calls to
-     * OrthancPluginReconstructMainDicomTags().
-     **/
-    virtual void UpgradeDatabase(uint32_t  targetVersion,
-                                 OrthancPluginStorageArea* storageArea) = 0;
-
-    virtual void ClearMainDicomTags(int64_t internalId) = 0;
-  };
-
-
-
-  /**
-   * @brief Bridge between C and C++ database engines.
-   * 
-   * Class creating the bridge between the C low-level primitives for
-   * custom database engines, and the high-level IDatabaseBackend C++
-   * interface.
-   *
-   * @ingroup Callbacks
-   **/
-  class DatabaseBackendAdapter
-  {
-  private:
-    // This class cannot be instantiated
-    DatabaseBackendAdapter()
-    {
-    }
-
-    static void LogError(IDatabaseBackend* backend,
-                         const std::runtime_error& e)
-    {
-      backend->GetOutput().LogError("Exception in database back-end: " + std::string(e.what()));
-    }
-
-
-    static OrthancPluginErrorCode  AddAttachment(void* payload,
-                                                 int64_t id,
-                                                 const OrthancPluginAttachment* attachment)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->AddAttachment(id, *attachment);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-                             
-    static OrthancPluginErrorCode  AttachChild(void* payload,
-                                               int64_t parent,
-                                               int64_t child)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->AttachChild(parent, child);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-          
-                   
-    static OrthancPluginErrorCode  ClearChanges(void* payload)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->ClearChanges();
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-                             
-
-    static OrthancPluginErrorCode  ClearExportedResources(void* payload)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->ClearExportedResources();
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  CreateResource(int64_t* id, 
-                                                  void* payload,
-                                                  const char* publicId,
-                                                  OrthancPluginResourceType resourceType)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        *id = backend->CreateResource(publicId, resourceType);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-          
-         
-    static OrthancPluginErrorCode  DeleteAttachment(void* payload,
-                                                    int64_t id,
-                                                    int32_t contentType)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->DeleteAttachment(id, contentType);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-   
-
-    static OrthancPluginErrorCode  DeleteMetadata(void* payload,
-                                                  int64_t id,
-                                                  int32_t metadataType)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->DeleteMetadata(id, metadataType);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-   
-
-    static OrthancPluginErrorCode  DeleteResource(void* payload,
-                                                  int64_t id)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->DeleteResource(id);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  GetAllInternalIds(OrthancPluginDatabaseContext* context,
-                                                     void* payload,
-                                                     OrthancPluginResourceType resourceType)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        std::list<int64_t> target;
-        backend->GetAllInternalIds(target, resourceType);
-
-        for (std::list<int64_t>::const_iterator
-               it = target.begin(); it != target.end(); ++it)
-        {
-          OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_,
-                                           backend->GetOutput().database_, *it);
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  GetAllPublicIds(OrthancPluginDatabaseContext* context,
-                                                   void* payload,
-                                                   OrthancPluginResourceType resourceType)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        std::list<std::string> ids;
-        backend->GetAllPublicIds(ids, resourceType);
-
-        for (std::list<std::string>::const_iterator
-               it = ids.begin(); it != ids.end(); ++it)
-        {
-          OrthancPluginDatabaseAnswerString(backend->GetOutput().context_,
-                                            backend->GetOutput().database_,
-                                            it->c_str());
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  GetAllPublicIdsWithLimit(OrthancPluginDatabaseContext* context,
-                                                            void* payload,
-                                                            OrthancPluginResourceType resourceType,
-                                                            uint64_t since,
-                                                            uint64_t limit)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        std::list<std::string> ids;
-        backend->GetAllPublicIds(ids, resourceType, since, limit);
-
-        for (std::list<std::string>::const_iterator
-               it = ids.begin(); it != ids.end(); ++it)
-        {
-          OrthancPluginDatabaseAnswerString(backend->GetOutput().context_,
-                                            backend->GetOutput().database_,
-                                            it->c_str());
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  GetChanges(OrthancPluginDatabaseContext* context,
-                                              void* payload,
-                                              int64_t since,
-                                              uint32_t maxResult)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_Change);
-
-      try
-      {
-        bool done;
-        backend->GetChanges(done, since, maxResult);
-        
-        if (done)
-        {
-          OrthancPluginDatabaseAnswerChangesDone(backend->GetOutput().context_,
-                                                 backend->GetOutput().database_);
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  GetChildrenInternalId(OrthancPluginDatabaseContext* context,
-                                                         void* payload,
-                                                         int64_t id)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        std::list<int64_t> target;
-        backend->GetChildrenInternalId(target, id);
-
-        for (std::list<int64_t>::const_iterator
-               it = target.begin(); it != target.end(); ++it)
-        {
-          OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_,
-                                           backend->GetOutput().database_, *it);
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-          
-         
-    static OrthancPluginErrorCode  GetChildrenPublicId(OrthancPluginDatabaseContext* context,
-                                                       void* payload,
-                                                       int64_t id)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        std::list<std::string> ids;
-        backend->GetChildrenPublicId(ids, id);
-
-        for (std::list<std::string>::const_iterator
-               it = ids.begin(); it != ids.end(); ++it)
-        {
-          OrthancPluginDatabaseAnswerString(backend->GetOutput().context_,
-                                            backend->GetOutput().database_,
-                                            it->c_str());
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  GetExportedResources(OrthancPluginDatabaseContext* context,
-                                                        void* payload,
-                                                        int64_t  since,
-                                                        uint32_t  maxResult)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_ExportedResource);
-
-      try
-      {
-        bool done;
-        backend->GetExportedResources(done, since, maxResult);
-
-        if (done)
-        {
-          OrthancPluginDatabaseAnswerExportedResourcesDone(backend->GetOutput().context_,
-                                                           backend->GetOutput().database_);
-        }
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-          
-         
-    static OrthancPluginErrorCode  GetLastChange(OrthancPluginDatabaseContext* context,
-                                                 void* payload)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_Change);
-
-      try
-      {
-        backend->GetLastChange();
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  GetLastExportedResource(OrthancPluginDatabaseContext* context,
-                                                           void* payload)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_ExportedResource);
-
-      try
-      {
-        backend->GetLastExportedResource();
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-    
-               
-    static OrthancPluginErrorCode  GetMainDicomTags(OrthancPluginDatabaseContext* context,
-                                                    void* payload,
-                                                    int64_t id)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_DicomTag);
-
-      try
-      {
-        backend->GetMainDicomTags(id);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-          
-         
-    static OrthancPluginErrorCode  GetPublicId(OrthancPluginDatabaseContext* context,
-                                               void* payload,
-                                               int64_t id)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        std::string s = backend->GetPublicId(id);
-        OrthancPluginDatabaseAnswerString(backend->GetOutput().context_,
-                                          backend->GetOutput().database_,
-                                          s.c_str());
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  GetResourceCount(uint64_t* target,
-                                                    void* payload,
-                                                    OrthancPluginResourceType  resourceType)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        *target = backend->GetResourceCount(resourceType);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-                   
-
-    static OrthancPluginErrorCode  GetResourceType(OrthancPluginResourceType* resourceType,
-                                                   void* payload,
-                                                   int64_t id)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        *resourceType = backend->GetResourceType(id);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  GetTotalCompressedSize(uint64_t* target,
-                                                          void* payload)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        *target = backend->GetTotalCompressedSize();
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-          
-         
-    static OrthancPluginErrorCode  GetTotalUncompressedSize(uint64_t* target,
-                                                            void* payload)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        *target = backend->GetTotalUncompressedSize();
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-                   
-
-    static OrthancPluginErrorCode  IsExistingResource(int32_t* existing,
-                                                      void* payload,
-                                                      int64_t id)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        *existing = backend->IsExistingResource(id);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  IsProtectedPatient(int32_t* isProtected,
-                                                      void* payload,
-                                                      int64_t id)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        *isProtected = backend->IsProtectedPatient(id);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  ListAvailableMetadata(OrthancPluginDatabaseContext* context,
-                                                         void* payload,
-                                                         int64_t id)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        std::list<int32_t> target;
-        backend->ListAvailableMetadata(target, id);
-
-        for (std::list<int32_t>::const_iterator
-               it = target.begin(); it != target.end(); ++it)
-        {
-          OrthancPluginDatabaseAnswerInt32(backend->GetOutput().context_,
-                                           backend->GetOutput().database_,
-                                           *it);
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-          
-         
-    static OrthancPluginErrorCode  ListAvailableAttachments(OrthancPluginDatabaseContext* context,
-                                                            void* payload,
-                                                            int64_t id)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        std::list<int32_t> target;
-        backend->ListAvailableAttachments(target, id);
-
-        for (std::list<int32_t>::const_iterator
-               it = target.begin(); it != target.end(); ++it)
-        {
-          OrthancPluginDatabaseAnswerInt32(backend->GetOutput().context_,
-                                           backend->GetOutput().database_,
-                                           *it);
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  LogChange(void* payload,
-                                             const OrthancPluginChange* change)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->LogChange(*change);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-          
-         
-    static OrthancPluginErrorCode  LogExportedResource(void* payload,
-                                                       const OrthancPluginExportedResource* exported)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->LogExportedResource(*exported);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-          
-         
-    static OrthancPluginErrorCode  LookupAttachment(OrthancPluginDatabaseContext* context,
-                                                    void* payload,
-                                                    int64_t id,
-                                                    int32_t contentType)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_Attachment);
-
-      try
-      {
-        backend->LookupAttachment(id, contentType);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  LookupGlobalProperty(OrthancPluginDatabaseContext* context,
-                                                        void* payload,
-                                                        int32_t property)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        std::string s;
-        if (backend->LookupGlobalProperty(s, property))
-        {
-          OrthancPluginDatabaseAnswerString(backend->GetOutput().context_,
-                                            backend->GetOutput().database_,
-                                            s.c_str());
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  LookupIdentifier3(OrthancPluginDatabaseContext* context,
-                                                     void* payload,
-                                                     OrthancPluginResourceType resourceType,
-                                                     const OrthancPluginDicomTag* tag,
-                                                     OrthancPluginIdentifierConstraint constraint)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        std::list<int64_t> target;
-        backend->LookupIdentifier(target, resourceType, tag->group, tag->element, constraint, tag->value);
-
-        for (std::list<int64_t>::const_iterator
-               it = target.begin(); it != target.end(); ++it)
-        {
-          OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_,
-                                           backend->GetOutput().database_, *it);
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  LookupMetadata(OrthancPluginDatabaseContext* context,
-                                                  void* payload,
-                                                  int64_t id,
-                                                  int32_t metadata)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        std::string s;
-        if (backend->LookupMetadata(s, id, metadata))
-        {
-          OrthancPluginDatabaseAnswerString(backend->GetOutput().context_,
-                                            backend->GetOutput().database_, s.c_str());
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  LookupParent(OrthancPluginDatabaseContext* context,
-                                                void* payload,
-                                                int64_t id)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        int64_t parent;
-        if (backend->LookupParent(parent, id))
-        {
-          OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_,
-                                           backend->GetOutput().database_, parent);
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  LookupResource(OrthancPluginDatabaseContext* context,
-                                                  void* payload,
-                                                  const char* publicId)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        int64_t id;
-        OrthancPluginResourceType type;
-        if (backend->LookupResource(id, type, publicId))
-        {
-          OrthancPluginDatabaseAnswerResource(backend->GetOutput().context_,
-                                              backend->GetOutput().database_, 
-                                              id, type);
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  SelectPatientToRecycle(OrthancPluginDatabaseContext* context,
-                                                          void* payload)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        int64_t id;
-        if (backend->SelectPatientToRecycle(id))
-        {
-          OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_,
-                                           backend->GetOutput().database_, id);
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  SelectPatientToRecycle2(OrthancPluginDatabaseContext* context,
-                                                           void* payload,
-                                                           int64_t patientIdToAvoid)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        int64_t id;
-        if (backend->SelectPatientToRecycle(id, patientIdToAvoid))
-        {
-          OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_,
-                                           backend->GetOutput().database_, id);
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  SetGlobalProperty(void* payload,
-                                                     int32_t property,
-                                                     const char* value)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->SetGlobalProperty(property, value);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  SetMainDicomTag(void* payload,
-                                                   int64_t id,
-                                                   const OrthancPluginDicomTag* tag)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->SetMainDicomTag(id, tag->group, tag->element, tag->value);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  SetIdentifierTag(void* payload,
-                                                    int64_t id,
-                                                    const OrthancPluginDicomTag* tag)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->SetIdentifierTag(id, tag->group, tag->element, tag->value);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  SetMetadata(void* payload,
-                                               int64_t id,
-                                               int32_t metadata,
-                                               const char* value)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->SetMetadata(id, metadata, value);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode  SetProtectedPatient(void* payload,
-                                                       int64_t id,
-                                                       int32_t isProtected)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->SetProtectedPatient(id, (isProtected != 0));
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode StartTransaction(void* payload)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->StartTransaction();
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode RollbackTransaction(void* payload)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->RollbackTransaction();
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode CommitTransaction(void* payload)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->CommitTransaction();
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode Open(void* payload)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->Open();
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode Close(void* payload)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
-
-      try
-      {
-        backend->Close();
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode GetDatabaseVersion(uint32_t* version,
-                                                     void* payload)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      
-      try
-      {
-        *version = backend->GetDatabaseVersion();
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-
-    static OrthancPluginErrorCode UpgradeDatabase(void* payload,
-                                                  uint32_t  targetVersion,
-                                                  OrthancPluginStorageArea* storageArea)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      
-      try
-      {
-        backend->UpgradeDatabase(targetVersion, storageArea);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-    
-    static OrthancPluginErrorCode ClearMainDicomTags(void* payload,
-                                                     int64_t internalId)
-    {
-      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
-      
-      try
-      {
-        backend->ClearMainDicomTags(internalId);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (std::runtime_error& e)
-      {
-        LogError(backend, e);
-        return OrthancPluginErrorCode_DatabasePlugin;
-      }
-      catch (DatabaseException& e)
-      {
-        return e.GetErrorCode();
-      }
-    }
-
-    
-  public:
-    /**
-     * Register a custom database back-end written in C++.
-     *
-     * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-     * @param backend Your custom database engine.
-     **/
-
-    static void Register(OrthancPluginContext* context,
-                         IDatabaseBackend& backend)
-    {
-      OrthancPluginDatabaseBackend  params;
-      memset(&params, 0, sizeof(params));
-
-      OrthancPluginDatabaseExtensions  extensions;
-      memset(&extensions, 0, sizeof(extensions));
-
-      params.addAttachment = AddAttachment;
-      params.attachChild = AttachChild;
-      params.clearChanges = ClearChanges;
-      params.clearExportedResources = ClearExportedResources;
-      params.createResource = CreateResource;
-      params.deleteAttachment = DeleteAttachment;
-      params.deleteMetadata = DeleteMetadata;
-      params.deleteResource = DeleteResource;
-      params.getAllPublicIds = GetAllPublicIds;
-      params.getChanges = GetChanges;
-      params.getChildrenInternalId = GetChildrenInternalId;
-      params.getChildrenPublicId = GetChildrenPublicId;
-      params.getExportedResources = GetExportedResources;
-      params.getLastChange = GetLastChange;
-      params.getLastExportedResource = GetLastExportedResource;
-      params.getMainDicomTags = GetMainDicomTags;
-      params.getPublicId = GetPublicId;
-      params.getResourceCount = GetResourceCount;
-      params.getResourceType = GetResourceType;
-      params.getTotalCompressedSize = GetTotalCompressedSize;
-      params.getTotalUncompressedSize = GetTotalUncompressedSize;
-      params.isExistingResource = IsExistingResource;
-      params.isProtectedPatient = IsProtectedPatient;
-      params.listAvailableMetadata = ListAvailableMetadata;
-      params.listAvailableAttachments = ListAvailableAttachments;
-      params.logChange = LogChange;
-      params.logExportedResource = LogExportedResource;
-      params.lookupAttachment = LookupAttachment;
-      params.lookupGlobalProperty = LookupGlobalProperty;
-      params.lookupIdentifier = NULL;   // Unused starting with Orthanc 0.9.5 (db v6)
-      params.lookupIdentifier2 = NULL;   // Unused starting with Orthanc 0.9.5 (db v6)
-      params.lookupMetadata = LookupMetadata;
-      params.lookupParent = LookupParent;
-      params.lookupResource = LookupResource;
-      params.selectPatientToRecycle = SelectPatientToRecycle;
-      params.selectPatientToRecycle2 = SelectPatientToRecycle2;
-      params.setGlobalProperty = SetGlobalProperty;
-      params.setMainDicomTag = SetMainDicomTag;
-      params.setIdentifierTag = SetIdentifierTag;
-      params.setMetadata = SetMetadata;
-      params.setProtectedPatient = SetProtectedPatient;
-      params.startTransaction = StartTransaction;
-      params.rollbackTransaction = RollbackTransaction;
-      params.commitTransaction = CommitTransaction;
-      params.open = Open;
-      params.close = Close;
-
-      extensions.getAllPublicIdsWithLimit = GetAllPublicIdsWithLimit;
-      extensions.getDatabaseVersion = GetDatabaseVersion;
-      extensions.upgradeDatabase = UpgradeDatabase;
-      extensions.clearMainDicomTags = ClearMainDicomTags;
-      extensions.getAllInternalIds = GetAllInternalIds;   // New in Orthanc 0.9.5 (db v6)
-      extensions.lookupIdentifier3 = LookupIdentifier3;   // New in Orthanc 0.9.5 (db v6)
-
-      OrthancPluginDatabaseContext* database = OrthancPluginRegisterDatabaseBackendV2(context, &params, &extensions, &backend);
-      if (!context)
-      {
-        throw std::runtime_error("Unable to register the database backend");
-      }
-
-      backend.RegisterOutput(new DatabaseBackendOutput(context, database));
-    }
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,8 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(Basic)
+
+set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
+include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake)
+
+add_library(AutomatedJpeg2kCompression SHARED Plugin.cpp)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/AutomatedJpeg2kCompression/Plugin.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,163 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include <orthanc/OrthancCPlugin.h>
+
+#include <string>
+
+static OrthancPluginContext* context_ = NULL;
+
+
+static bool ReadFile(std::string& result,
+                     const std::string& path)
+{
+  OrthancPluginMemoryBuffer tmp;
+  if (OrthancPluginReadFile(context_, &tmp, path.c_str()) == OrthancPluginErrorCode_Success)
+  {
+    result.assign(reinterpret_cast<const char*>(tmp.data), tmp.size);
+    OrthancPluginFreeMemoryBuffer(context_, &tmp);
+    return true;
+  }
+  else
+  {
+    return false;
+  }
+}
+
+
+OrthancPluginErrorCode OnStoredCallback(OrthancPluginDicomInstance* instance,
+                                        const char* instanceId)
+{
+  char buffer[1024];
+  sprintf(buffer, "Just received a DICOM instance of size %d and ID %s from origin %d (AET %s)", 
+          (int) OrthancPluginGetInstanceSize(context_, instance), instanceId, 
+          OrthancPluginGetInstanceOrigin(context_, instance),
+          OrthancPluginGetInstanceRemoteAet(context_, instance));
+  OrthancPluginLogInfo(context_, buffer);
+
+  if (OrthancPluginGetInstanceOrigin(context_, instance) == OrthancPluginInstanceOrigin_Plugin)
+  {
+    // Do not compress twice the same file
+    return OrthancPluginErrorCode_Success;
+  }
+
+  // Write the uncompressed DICOM content to some temporary file
+  std::string uncompressed = "uncompressed-" + std::string(instanceId) + ".dcm";
+  OrthancPluginErrorCode error = OrthancPluginWriteFile(context_, uncompressed.c_str(), 
+                                                        OrthancPluginGetInstanceData(context_, instance),
+                                                        OrthancPluginGetInstanceSize(context_, instance));
+  if (error)
+  {
+    return error;
+  }
+
+  // Remove the original DICOM instance
+  std::string uri = "/instances/" + std::string(instanceId);
+  error = OrthancPluginRestApiDelete(context_, uri.c_str());
+  if (error)
+  {
+    return error;
+  }
+
+  // Path to the temporary file that will contain the compressed DICOM content
+  std::string compressed = "compressed-" + std::string(instanceId) + ".dcm";
+
+  // Compress to JPEG2000 using gdcm
+  std::string command1 = "gdcmconv --j2k " + uncompressed + " " + compressed;
+
+  // Generate a new SOPInstanceUID for the JPEG2000 file, as gdcmconv
+  // does not do this by itself
+  std::string command2 = "dcmodify --no-backup -gin " + compressed;
+
+  // Make the required system calls
+  system(command1.c_str());
+  system(command2.c_str());
+
+  // Read the result of the JPEG2000 compression
+  std::string j2k;
+  bool ok = ReadFile(j2k, compressed);
+
+  // Remove the two temporary files
+  remove(compressed.c_str());
+  remove(uncompressed.c_str());
+
+  if (!ok)
+  {
+    return OrthancPluginErrorCode_Plugin;
+  }
+
+  // Upload the JPEG2000 file through the REST API
+  OrthancPluginMemoryBuffer tmp;
+  if (OrthancPluginRestApiPost(context_, &tmp, "/instances", j2k.c_str(), j2k.size()))
+  {
+    ok = false;
+  }
+
+  if (ok)
+  {
+    OrthancPluginFreeMemoryBuffer(context_, &tmp);
+  }
+
+  return ok ? OrthancPluginErrorCode_Success : OrthancPluginErrorCode_Plugin;
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    context_ = c;
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(c) == 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;
+    }
+
+    OrthancPluginRegisterOnStoredInstanceCallback(context_, OnStoredCallback);
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "sample-jpeg2k";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return "0.0";
+  }
+}
--- a/Plugins/Samples/Basic/Plugin.c	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Samples/Basic/Plugin.c	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -28,9 +29,9 @@
 static OrthancPluginErrorCode customError;
 
 
-ORTHANC_PLUGINS_API int32_t Callback1(OrthancPluginRestOutput* output,
-                                      const char* url,
-                                      const OrthancPluginHttpRequest* request)
+ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback1(OrthancPluginRestOutput* output,
+                                                     const char* url,
+                                                     const OrthancPluginHttpRequest* request)
 {
   char buffer[1024];
   uint32_t i;
@@ -57,7 +58,7 @@
     OrthancPluginLogWarning(context, buffer);    
   }
 
-  OrthancPluginLogWarning(context, "");    
+  OrthancPluginLogWarning(context, "");
 
   for (i = 0; i < request->headersCount; i++)
   {
@@ -67,13 +68,13 @@
 
   OrthancPluginLogWarning(context, "");
 
-  return 1;
+  return OrthancPluginErrorCode_Success;
 }
 
 
-ORTHANC_PLUGINS_API int32_t Callback2(OrthancPluginRestOutput* output,
-                                      const char* url,
-                                      const OrthancPluginHttpRequest* request)
+ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback2(OrthancPluginRestOutput* output,
+                                                     const char* url,
+                                                     const OrthancPluginHttpRequest* request)
 {
   /* Answer with a sample 16bpp image. */
 
@@ -99,13 +100,13 @@
                                            256, 256, sizeof(uint16_t) * 256, buffer);
   }
 
-  return 0;
+  return OrthancPluginErrorCode_Success;
 }
 
 
-ORTHANC_PLUGINS_API int32_t Callback3(OrthancPluginRestOutput* output,
-                                      const char* url,
-                                      const OrthancPluginHttpRequest* request)
+ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback3(OrthancPluginRestOutput* output,
+                                                     const char* url,
+                                                     const OrthancPluginHttpRequest* request)
 {
   if (request->method != OrthancPluginHttpMethod_Get)
   {
@@ -124,13 +125,13 @@
     }
   }
 
-  return 0;
+  return OrthancPluginErrorCode_Success;
 }
 
 
-ORTHANC_PLUGINS_API int32_t Callback4(OrthancPluginRestOutput* output,
-                                      const char* url,
-                                      const OrthancPluginHttpRequest* request)
+ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback4(OrthancPluginRestOutput* output,
+                                                     const char* url,
+                                                     const OrthancPluginHttpRequest* request)
 {
   /* Answer with a sample 8bpp image. */
 
@@ -156,13 +157,13 @@
                                            256, 256, 256, buffer);
   }
 
-  return 0;
+  return OrthancPluginErrorCode_Success;
 }
 
 
-ORTHANC_PLUGINS_API int32_t Callback5(OrthancPluginRestOutput* output,
-                                      const char* url,
-                                      const OrthancPluginHttpRequest* request)
+ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback5(OrthancPluginRestOutput* output,
+                                                     const char* url,
+                                                     const OrthancPluginHttpRequest* request)
 {
   /**
    * Demonstration the difference between the
@@ -194,20 +195,18 @@
   }
   else
   {
-    printf("ICI1\n");
     error = OrthancPluginRestApiGetAfterPlugins(context, &tmp, request->groups[1]);
-    printf("ICI2\n");
   }
 
   if (error)
   {
-    return -1;
+    return OrthancPluginErrorCode_InternalError;
   }
   else
   {
     OrthancPluginAnswerBuffer(context, output, tmp.data, tmp.size, "application/octet-stream");
     OrthancPluginFreeMemoryBuffer(context, &tmp);
-    return 0;
+    return OrthancPluginErrorCode_Success;
   }
 }
 
@@ -264,8 +263,9 @@
   char* json;
   static int first = 1;
 
-  sprintf(buffer, "Just received a DICOM instance of size %d and ID %s from AET %s", 
+  sprintf(buffer, "Just received a DICOM instance of size %d and ID %s from origin %d (AET %s)", 
           (int) OrthancPluginGetInstanceSize(context, instance), instanceId, 
+          OrthancPluginGetInstanceOrigin(context, instance),
           OrthancPluginGetInstanceRemoteAet(context, instance));
 
   OrthancPluginLogWarning(context, buffer);  
@@ -323,11 +323,44 @@
 }
 
 
+ORTHANC_PLUGINS_API int32_t FilterIncomingHttpRequest(OrthancPluginHttpMethod  method,
+                                                      const char*              uri,
+                                                      const char*              ip,
+                                                      uint32_t                 headersCount,
+                                                      const char* const*       headersKeys,
+                                                      const char* const*       headersValues)
+{
+  uint32_t i;
+
+  if (headersCount > 0)
+  {
+    OrthancPluginLogInfo(context, "HTTP headers of an incoming REST request:");
+    for (i = 0; i < headersCount; i++)
+    {
+      char info[1024];
+      sprintf(info, "  %s: %s", headersKeys[i], headersValues[i]);
+      OrthancPluginLogInfo(context, info);
+    }
+  }
+
+  if (method == OrthancPluginHttpMethod_Get ||
+      method == OrthancPluginHttpMethod_Post)
+  {
+    return 1;  /* Allowed */
+  }
+  else
+  {
+    return 0;  /* Only allow GET and POST requests */
+  }
+}
+
+
 ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
 {
   OrthancPluginMemoryBuffer tmp;
   char info[1024], *s;
   int counter, i;
+  OrthancPluginDictionaryEntry entry;
 
   context = c;
   OrthancPluginLogWarning(context, "Sample plugin is initializing");
@@ -384,8 +417,8 @@
   OrthancPluginRegisterRestCallback(context, "/plugin/create", CallbackCreateDicom);
 
   OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback);
-
   OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
+  OrthancPluginRegisterIncomingHttpRequestFilter(context, FilterIncomingHttpRequest);
 
   /* Declare several properties of the plugin */
   OrthancPluginSetRootUri(context, "/plugin/hello");
@@ -408,6 +441,9 @@
   OrthancPluginRegisterDictionaryTag(context, 0x0014, 0x1020, OrthancPluginValueRepresentation_DA,
                                      "ValidationExpiryDate", 1, 1);
 
+  OrthancPluginLookupDictionary(context, &entry, "ValidationExpiryDate");
+  OrthancPluginLookupDictionary(context, &entry, "0010-0010");
+
   return 0;
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/DicomDatasetReader.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,173 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomDatasetReader.h"
+
+#include "OrthancPluginException.h"
+
+#include <boost/lexical_cast.hpp>
+
+namespace OrthancPlugins
+{
+  // This function is copied-pasted from "../../../Core/Toolbox.cpp",
+  // in order to avoid the dependency of plugins against the Orthanc core
+  static std::string StripSpaces(const std::string& source)
+  {
+    size_t first = 0;
+
+    while (first < source.length() &&
+           isspace(source[first]))
+    {
+      first++;
+    }
+
+    if (first == source.length())
+    {
+      // String containing only spaces
+      return "";
+    }
+
+    size_t last = source.length();
+    while (last > first &&
+           isspace(source[last - 1]))
+    {
+      last--;
+    }          
+    
+    assert(first <= last);
+    return source.substr(first, last - first);
+  }
+
+
+  DicomDatasetReader::DicomDatasetReader(const IDicomDataset& dataset) :
+    dataset_(dataset)
+  {
+  }
+  
+
+  std::string DicomDatasetReader::GetStringValue(const DicomPath& path,
+                                                 const std::string& defaultValue) const
+  {
+    std::string s;
+    if (dataset_.GetStringValue(s, path))
+    {
+      return s;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  std::string DicomDatasetReader::GetMandatoryStringValue(const DicomPath& path) const
+  {
+    std::string s;
+    if (dataset_.GetStringValue(s, path))
+    {
+      return s;
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InexistentTag);
+    }
+  }
+
+
+  template <typename T>
+  static bool GetValueInternal(T& target,
+                               const IDicomDataset& dataset,
+                               const DicomPath& path)
+  {
+    try
+    {
+      std::string s;
+
+      if (dataset.GetStringValue(s, path))
+      {
+        target = boost::lexical_cast<T>(StripSpaces(s));
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);        
+    }
+  }
+
+
+  bool DicomDatasetReader::GetIntegerValue(int& target,
+                                           const DicomPath& path) const
+  {
+    return GetValueInternal<int>(target, dataset_, path);
+  }
+
+
+  bool DicomDatasetReader::GetUnsignedIntegerValue(unsigned int& target,
+                                                   const DicomPath& path) const
+  {
+    int value;
+
+    if (!GetIntegerValue(value, path))
+    {
+      return false;
+    }
+    else if (value >= 0)
+    {
+      target = static_cast<unsigned int>(value);
+      return true;
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+  }
+
+
+  bool DicomDatasetReader::GetFloatValue(float& target,
+                                         const DicomPath& path) const
+  {
+    return GetValueInternal<float>(target, dataset_, path);
+  }
+
+
+  bool DicomDatasetReader::GetDoubleValue(double& target,
+                                          const DicomPath& path) const
+  {
+    return GetValueInternal<double>(target, dataset_, path);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/DicomDatasetReader.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IDicomDataset.h"
+
+#include <memory>
+#include <vector>
+
+namespace OrthancPlugins
+{
+  class DicomDatasetReader : public boost::noncopyable
+  {
+  private:
+    const IDicomDataset&  dataset_;
+
+  public:
+    DicomDatasetReader(const IDicomDataset& dataset);
+
+    const IDicomDataset& GetDataset() const
+    {
+      return dataset_;
+    }
+
+    std::string GetStringValue(const DicomPath& path,
+                               const std::string& defaultValue) const;
+
+    std::string GetMandatoryStringValue(const DicomPath& path) const;
+
+    bool GetIntegerValue(int& target,
+                         const DicomPath& path) const;
+
+    bool GetUnsignedIntegerValue(unsigned int& target,
+                                 const DicomPath& path) const;
+
+    bool GetFloatValue(float& target,
+                       const DicomPath& path) const;
+
+    bool GetDoubleValue(double& target,
+                        const DicomPath& path) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/DicomPath.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,87 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomPath.h"
+
+#include "OrthancPluginException.h"
+
+namespace OrthancPlugins
+{
+  const DicomPath::Prefix& DicomPath::GetPrefixItem(size_t depth) const
+  {
+    if (depth >= prefix_.size())
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+    else
+    {
+      return prefix_[depth];
+    }
+  }
+
+
+  DicomPath::DicomPath(const DicomTag& sequence,
+                       size_t index,
+                       const DicomTag& tag) :
+    finalTag_(tag)
+  {
+    AddToPrefix(sequence, index);
+  }
+
+
+  DicomPath::DicomPath(const DicomTag& sequence1,
+                       size_t index1,
+                       const DicomTag& sequence2,
+                       size_t index2,
+                       const DicomTag& tag) :
+    finalTag_(tag)
+  {
+    AddToPrefix(sequence1, index1);
+    AddToPrefix(sequence2, index2);
+  }
+
+
+  DicomPath::DicomPath(const DicomTag& sequence1,
+                       size_t index1,
+                       const DicomTag& sequence2,
+                       size_t index2,
+                       const DicomTag& sequence3,
+                       size_t index3,
+                       const DicomTag& tag) :
+    finalTag_(tag)
+  {
+    AddToPrefix(sequence1, index1);
+    AddToPrefix(sequence2, index2);
+    AddToPrefix(sequence3, index3);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/DicomPath.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,108 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomTag.h"
+
+#include <vector>
+#include <stddef.h>
+
+namespace OrthancPlugins
+{
+  class DicomPath
+  {
+  private:
+    typedef std::pair<DicomTag, size_t>  Prefix;
+
+    std::vector<Prefix>  prefix_;
+    DicomTag             finalTag_;
+
+    const Prefix& GetPrefixItem(size_t depth) const;
+
+  public:
+    DicomPath(const DicomTag& finalTag) :
+    finalTag_(finalTag)
+    {
+    }
+
+    DicomPath(const DicomTag& sequence,
+              size_t index,
+              const DicomTag& tag);
+
+    DicomPath(const DicomTag& sequence1,
+              size_t index1,
+              const DicomTag& sequence2,
+              size_t index2,
+              const DicomTag& tag);
+
+    DicomPath(const DicomTag& sequence1,
+              size_t index1,
+              const DicomTag& sequence2,
+              size_t index2,
+              const DicomTag& sequence3,
+              size_t index3,
+              const DicomTag& tag);
+
+    void AddToPrefix(const DicomTag& tag,
+                     size_t position)
+    {
+      prefix_.push_back(std::make_pair(tag, position));
+    }
+
+    size_t GetPrefixLength() const
+    {
+      return prefix_.size();
+    }
+    
+    DicomTag GetPrefixTag(size_t depth) const
+    {
+      return GetPrefixItem(depth).first;
+    }
+
+    size_t GetPrefixIndex(size_t depth) const
+    {
+      return GetPrefixItem(depth).second;
+    }
+    
+    const DicomTag& GetFinalTag() const
+    {
+      return finalTag_;
+    }
+
+    void SetFinalTag(const DicomTag& tag)
+    {
+      finalTag_ = tag;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/DicomTag.cpp	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomTag.h"
+
+#include "OrthancPluginException.h"
+
+namespace OrthancPlugins
+{
+  const char* DicomTag::GetName() const
+  {
+    if (*this == DICOM_TAG_BITS_STORED)
+    {
+      return "BitsStored";
+    }
+    else if (*this == DICOM_TAG_COLUMN_POSITION_IN_TOTAL_IMAGE_PIXEL_MATRIX)
+    {
+      return "ColumnPositionInTotalImagePixelMatrix";
+    }
+    else if (*this == DICOM_TAG_COLUMNS)
+    {
+      return "Columns";
+    }
+    else if (*this == DICOM_TAG_MODALITY)
+    {
+      return "Modality";
+    }
+    else if (*this == DICOM_TAG_NUMBER_OF_FRAMES)
+    {
+      return "NumberOfFrames";
+    }
+    else if (*this == DICOM_TAG_PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE)
+    {
+      return "PerFrameFunctionalGroupsSequence";
+    }
+    else if (*this == DICOM_TAG_PHOTOMETRIC_INTERPRETATION)
+    {
+      return "PhotometricInterpretation";
+    }
+    else if (*this == DICOM_TAG_PIXEL_REPRESENTATION)
+    {
+      return "PixelRepresentation";
+    }
+    else if (*this == DICOM_TAG_PLANE_POSITION_SLIDE_SEQUENCE)
+    {
+      return "PlanePositionSlideSequence";
+    }
+    else if (*this == DICOM_TAG_ROW_POSITION_IN_TOTAL_IMAGE_PIXEL_MATRIX)
+    {
+      return "RowPositionInTotalImagePixelMatrix";
+    }
+    else if (*this == DICOM_TAG_ROWS)
+    {
+      return "Rows";
+    }
+    else if (*this == DICOM_TAG_SOP_CLASS_UID)
+    {
+      return "SOPClassUID";
+    }
+    else if (*this == DICOM_TAG_SAMPLES_PER_PIXEL)
+    {
+      return "SamplesPerPixel";
+    }
+    else if (*this == DICOM_TAG_TOTAL_PIXEL_MATRIX_COLUMNS)
+    {
+      return "TotalPixelMatrixColumns";
+    }
+    else if (*this == DICOM_TAG_TOTAL_PIXEL_MATRIX_ROWS)
+    {
+      return "TotalPixelMatrixRows";
+    }
+    else if (*this == DICOM_TAG_TRANSFER_SYNTAX_UID)
+    {
+      return "TransferSyntaxUID";
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotImplemented);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/DicomTag.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,106 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <stdint.h>
+
+namespace OrthancPlugins
+{
+  class DicomTag
+  {
+  private:
+    uint16_t  group_;
+    uint16_t  element_;
+
+    DicomTag();  // Forbidden
+
+  public:
+    DicomTag(uint16_t group,
+             uint16_t element) :
+      group_(group),
+      element_(element)
+    {
+    }
+
+    uint16_t GetGroup() const
+    {
+      return group_;
+    }
+
+    uint16_t GetElement() const
+    {
+      return element_;
+    }
+
+    const char* GetName() const;
+
+    bool operator== (const DicomTag& other) const
+    {
+      return group_ == other.group_ && element_ == other.element_;
+    }
+
+    bool operator!= (const DicomTag& other) const
+    {
+      return !(*this == other);
+    }
+  };
+
+
+  static const DicomTag DICOM_TAG_BITS_STORED(0x0028, 0x0101);
+  static const DicomTag DICOM_TAG_COLUMNS(0x0028, 0x0011);
+  static const DicomTag DICOM_TAG_COLUMN_POSITION_IN_TOTAL_IMAGE_PIXEL_MATRIX(0x0048, 0x021e);
+  static const DicomTag DICOM_TAG_IMAGE_ORIENTATION_PATIENT(0x0020, 0x0037);
+  static const DicomTag DICOM_TAG_IMAGE_POSITION_PATIENT(0x0020, 0x0032);
+  static const DicomTag DICOM_TAG_MODALITY(0x0008, 0x0060);
+  static const DicomTag DICOM_TAG_NUMBER_OF_FRAMES(0x0028, 0x0008);
+  static const DicomTag DICOM_TAG_PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE(0x5200, 0x9230);
+  static const DicomTag DICOM_TAG_PHOTOMETRIC_INTERPRETATION(0x0028, 0x0004);
+  static const DicomTag DICOM_TAG_PIXEL_REPRESENTATION(0x0028, 0x0103);
+  static const DicomTag DICOM_TAG_PIXEL_SPACING(0x0028, 0x0030);
+  static const DicomTag DICOM_TAG_PLANE_POSITION_SLIDE_SEQUENCE(0x0048, 0x021a);
+  static const DicomTag DICOM_TAG_RESCALE_INTERCEPT(0x0028, 0x1052);
+  static const DicomTag DICOM_TAG_RESCALE_SLOPE(0x0028, 0x1053);
+  static const DicomTag DICOM_TAG_ROWS(0x0028, 0x0010);
+  static const DicomTag DICOM_TAG_ROW_POSITION_IN_TOTAL_IMAGE_PIXEL_MATRIX(0x0048, 0x021f);
+  static const DicomTag DICOM_TAG_SAMPLES_PER_PIXEL(0x0028, 0x0002);
+  static const DicomTag DICOM_TAG_SERIES_INSTANCE_UID(0x0020, 0x000e);
+  static const DicomTag DICOM_TAG_SLICE_THICKNESS(0x0018, 0x0050);
+  static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016);
+  static const DicomTag DICOM_TAG_SOP_INSTANCE_UID(0x0008, 0x0018);
+  static const DicomTag DICOM_TAG_TOTAL_PIXEL_MATRIX_COLUMNS(0x0048, 0x0006);
+  static const DicomTag DICOM_TAG_TOTAL_PIXEL_MATRIX_ROWS(0x0048, 0x0007);
+  static const DicomTag DICOM_TAG_TRANSFER_SYNTAX_UID(0x0002, 0x0010);
+  static const DicomTag DICOM_TAG_WINDOW_CENTER(0x0028, 0x1050);
+  static const DicomTag DICOM_TAG_WINDOW_WIDTH(0x0028, 0x1051);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/FullOrthancDataset.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,215 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "FullOrthancDataset.h"
+
+#include "OrthancPluginException.h"
+
+#include <stdio.h>
+#include <cassert>
+
+namespace OrthancPlugins
+{
+  static const Json::Value* AccessTag(const Json::Value& dataset,
+                                      const DicomTag& tag) 
+  {
+    if (dataset.type() != Json::objectValue)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+
+    char name[16];
+    sprintf(name, "%04x,%04x", tag.GetGroup(), tag.GetElement());
+
+    if (!dataset.isMember(name))
+    {
+      return NULL;
+    }
+
+    const Json::Value& value = dataset[name];
+    if (value.type() != Json::objectValue ||
+        !value.isMember("Name") ||
+        !value.isMember("Type") ||
+        !value.isMember("Value") ||
+        value["Name"].type() != Json::stringValue ||
+        value["Type"].type() != Json::stringValue)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+
+    return &value;
+  }
+
+
+  static const Json::Value& GetSequenceContent(const Json::Value& sequence)
+  {
+    assert(sequence.type() == Json::objectValue);
+    assert(sequence.isMember("Type"));
+    assert(sequence.isMember("Value"));
+
+    const Json::Value& value = sequence["Value"];
+      
+    if (sequence["Type"].asString() != "Sequence" ||
+        value.type() != Json::arrayValue)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+    else
+    {
+      return value;
+    }
+  }
+
+
+  static bool GetStringInternal(std::string& result,
+                                const Json::Value& tag)
+  {
+    assert(tag.type() == Json::objectValue);
+    assert(tag.isMember("Type"));
+    assert(tag.isMember("Value"));
+
+    const Json::Value& value = tag["Value"];
+      
+    if (tag["Type"].asString() != "String" ||
+        value.type() != Json::stringValue)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+    else
+    {
+      result = value.asString();
+      return true;
+    }
+  }
+
+
+  const Json::Value* FullOrthancDataset::LookupPath(const DicomPath& path) const
+  {
+    const Json::Value* content = &root_;
+                                  
+    for (unsigned int depth = 0; depth < path.GetPrefixLength(); depth++)
+    {
+      const Json::Value* sequence = AccessTag(*content, path.GetPrefixTag(depth));
+      if (sequence == NULL)
+      {
+        return NULL;
+      }
+
+      const Json::Value& nextContent = GetSequenceContent(*sequence);
+
+      size_t index = path.GetPrefixIndex(depth);
+      if (index >= nextContent.size())
+      {
+        return NULL;
+      }
+      else
+      {
+        content = &nextContent[static_cast<Json::Value::ArrayIndex>(index)];
+      }
+    }
+
+    return AccessTag(*content, path.GetFinalTag());
+  }
+
+
+  void FullOrthancDataset::CheckRoot() const
+  {
+    if (root_.type() != Json::objectValue)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+  }
+
+
+  FullOrthancDataset::FullOrthancDataset(IOrthancConnection& orthanc,
+                                         const std::string& uri)
+  {
+    IOrthancConnection::RestApiGet(root_, orthanc, uri);
+    CheckRoot();
+  }
+
+
+  FullOrthancDataset::FullOrthancDataset(const std::string& content)
+  {
+    IOrthancConnection::ParseJson(root_, content);
+    CheckRoot();
+  }
+
+
+  FullOrthancDataset::FullOrthancDataset(const void* content,
+                                         size_t size)
+  {
+    IOrthancConnection::ParseJson(root_, content, size);
+    CheckRoot();
+  }
+
+
+  FullOrthancDataset::FullOrthancDataset(const Json::Value& root) :
+    root_(root)
+  {
+    CheckRoot();
+  }
+
+
+  bool FullOrthancDataset::GetStringValue(std::string& result,
+                                          const DicomPath& path) const
+  {
+    const Json::Value* value = LookupPath(path);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return GetStringInternal(result, *value);
+    }
+  }
+
+
+  bool FullOrthancDataset::GetSequenceSize(size_t& size,
+                                           const DicomPath& path) const
+  {
+    const Json::Value* sequence = LookupPath(path);
+
+    if (sequence == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      size = GetSequenceContent(*sequence).size();
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/FullOrthancDataset.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,69 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IOrthancConnection.h"
+#include "IDicomDataset.h"
+
+#include <json/value.h>
+
+namespace OrthancPlugins
+{
+  class FullOrthancDataset : public IDicomDataset
+  {
+  private:
+    Json::Value   root_;
+
+    const Json::Value* LookupPath(const DicomPath& path) const;
+
+    void CheckRoot() const;
+
+  public:
+    FullOrthancDataset(IOrthancConnection& orthanc,
+                       const std::string& uri);
+
+    FullOrthancDataset(const std::string& content);
+
+    FullOrthancDataset(const void* content,
+                       size_t size);
+
+    FullOrthancDataset(const Json::Value& root);
+
+    virtual bool GetStringValue(std::string& result,
+                                const DicomPath& path) const;
+
+    virtual bool GetSequenceSize(size_t& size,
+                                 const DicomPath& path) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/IDicomDataset.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,56 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomPath.h"
+
+#include <boost/noncopyable.hpp>
+#include <string>
+
+namespace OrthancPlugins
+{
+  class IDicomDataset : public boost::noncopyable
+  {
+  public:
+    virtual ~IDicomDataset()
+    {
+    }
+
+    virtual bool GetStringValue(std::string& result,
+                                const DicomPath& path) const = 0;
+
+    virtual bool GetSequenceSize(size_t& size,
+                                 const DicomPath& path) const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/IOrthancConnection.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,98 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "IOrthancConnection.h"
+
+#include "OrthancPluginException.h"
+
+#include <json/reader.h>
+
+namespace OrthancPlugins
+{
+  void IOrthancConnection::ParseJson(Json::Value& result,
+                                     const std::string& content)
+  {
+    Json::Reader reader;
+    
+    if (!reader.parse(content, result))
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+  }
+
+
+  void IOrthancConnection::ParseJson(Json::Value& result,
+                                     const void* content,
+                                     size_t size)
+  {
+    Json::Reader reader;
+    
+    if (!reader.parse(reinterpret_cast<const char*>(content),
+                      reinterpret_cast<const char*>(content) + size, result))
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+  }
+
+
+  void IOrthancConnection::RestApiGet(Json::Value& result,
+                                      IOrthancConnection& orthanc,
+                                      const std::string& uri)
+  {
+    std::string content;
+    orthanc.RestApiGet(content, uri);
+    ParseJson(result, content);
+  }
+
+
+  void IOrthancConnection::RestApiPost(Json::Value& result,
+                                       IOrthancConnection& orthanc,
+                                       const std::string& uri,
+                                       const std::string& body)
+  {
+    std::string content;
+    orthanc.RestApiPost(content, uri, body);
+    ParseJson(result, content);
+  }
+
+
+  void IOrthancConnection::RestApiPut(Json::Value& result,
+                                      IOrthancConnection& orthanc,
+                                      const std::string& uri,
+                                      const std::string& body)
+  {
+    std::string content;
+    orthanc.RestApiPut(content, uri, body);
+    ParseJson(result, content);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/IOrthancConnection.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomPath.h"
+
+#include <boost/noncopyable.hpp>
+#include <string>
+#include <json/value.h>
+
+namespace OrthancPlugins
+{
+  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;
+
+    virtual void RestApiPut(std::string& result,
+                            const std::string& uri,
+                            const std::string& body) = 0;
+
+    virtual void RestApiDelete(const std::string& uri) = 0;
+
+    static void ParseJson(Json::Value& result,
+                          const std::string& content);
+
+    static void ParseJson(Json::Value& result,
+                          const void* content,
+                          size_t size);
+
+    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);
+
+    static void RestApiPut(Json::Value& result,
+                           IOrthancConnection& orthanc,
+                           const std::string& uri,
+                           const std::string& body);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/OrthancHttpConnection.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,108 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OrthancHttpConnection.h"
+
+namespace OrthancPlugins
+{
+  void OrthancHttpConnection::Setup()
+  {
+    url_ = client_.GetUrl();
+
+    // Don't follow 3xx HTTP (avoid redirections to "unsupported.png" in Orthanc)
+    client_.SetRedirectionFollowed(false);  
+  }
+
+
+  OrthancHttpConnection::OrthancHttpConnection() :
+    client_(Orthanc::WebServiceParameters(), "")
+  {
+    Setup();
+  }
+
+
+  OrthancHttpConnection::OrthancHttpConnection(const Orthanc::WebServiceParameters& parameters) :
+    client_(parameters, "")
+  {
+    Setup();
+  }
+
+
+  void OrthancHttpConnection::RestApiGet(std::string& result,
+                                         const std::string& uri)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    client_.SetMethod(Orthanc::HttpMethod_Get);
+    client_.SetUrl(url_ + uri);
+    client_.ApplyAndThrowException(result);
+  }
+
+
+  void OrthancHttpConnection::RestApiPost(std::string& result,
+                                          const std::string& uri,
+                                          const std::string& body)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    client_.SetMethod(Orthanc::HttpMethod_Post);
+    client_.SetUrl(url_ + uri);
+    client_.SetBody(body);
+    client_.ApplyAndThrowException(result);
+  }
+
+
+  void OrthancHttpConnection::RestApiPut(std::string& result,
+                                         const std::string& uri,
+                                         const std::string& body)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    client_.SetMethod(Orthanc::HttpMethod_Put);
+    client_.SetUrl(url_ + uri);
+    client_.SetBody(body);
+    client_.ApplyAndThrowException(result);
+  }
+
+
+  void OrthancHttpConnection::RestApiDelete(const std::string& uri)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    std::string result;
+
+    client_.SetMethod(Orthanc::HttpMethod_Delete);
+    client_.SetUrl(url_ + uri);
+    client_.ApplyAndThrowException(result);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/OrthancHttpConnection.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IOrthancConnection.h"
+
+#if HAS_ORTHANC_EXCEPTION != 1
+#  error The macro HAS_ORTHANC_EXCEPTION must be set to 1 if using this header
+#endif
+
+#include "../../../Core/HttpClient.h"
+
+#include <boost/thread/mutex.hpp>
+
+namespace OrthancPlugins
+{
+  // This class is thread-safe
+  class OrthancHttpConnection : public IOrthancConnection
+  {
+  private:
+    boost::mutex         mutex_;
+    Orthanc::HttpClient  client_;
+    std::string          url_;
+
+    void Setup();
+
+  public:
+    OrthancHttpConnection();
+
+    OrthancHttpConnection(const Orthanc::WebServiceParameters& parameters);
+
+    virtual void RestApiGet(std::string& result,
+                            const std::string& uri);
+
+    virtual void RestApiPost(std::string& result,
+                             const std::string& uri,
+                             const std::string& body);
+
+    virtual void RestApiPut(std::string& result,
+                            const std::string& uri,
+                            const std::string& body);
+
+    virtual void RestApiDelete(const std::string& uri);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/OrthancPluginConnection.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,99 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OrthancPluginConnection.h"
+
+#include "OrthancPluginCppWrapper.h"
+
+namespace OrthancPlugins
+{
+  void OrthancPluginConnection::RestApiGet(std::string& result,
+                                           const std::string& uri) 
+  {
+    OrthancPlugins::MemoryBuffer buffer(context_);
+
+    if (buffer.RestApiGet(uri, false))
+    {
+      buffer.ToString(result);
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource);
+    }
+  }
+
+
+  void OrthancPluginConnection::RestApiPost(std::string& result,
+                                            const std::string& uri,
+                                            const std::string& body)
+  {
+    OrthancPlugins::MemoryBuffer buffer(context_);
+
+    if (buffer.RestApiPost(uri, body.c_str(), body.size(), false))
+    {
+      buffer.ToString(result);
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource);
+    }
+  }
+
+
+  void OrthancPluginConnection::RestApiPut(std::string& result,
+                                           const std::string& uri,
+                                           const std::string& body)
+  {
+    OrthancPlugins::MemoryBuffer buffer(context_);
+
+    if (buffer.RestApiPut(uri, body.c_str(), body.size(), false))
+    {
+      buffer.ToString(result);
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource);
+    }
+  }
+
+
+  void OrthancPluginConnection::RestApiDelete(const std::string& uri)
+  {
+    OrthancPlugins::MemoryBuffer buffer(context_);
+
+    if (!::OrthancPlugins::RestApiDelete(context_, uri, false))
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/OrthancPluginConnection.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,67 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IOrthancConnection.h"
+
+#include <orthanc/OrthancCPlugin.h>
+
+namespace OrthancPlugins
+{
+  // This class is thread-safe
+  class OrthancPluginConnection : public IOrthancConnection
+  {
+  private:
+    OrthancPluginContext*   context_;
+
+  public:
+    OrthancPluginConnection(OrthancPluginContext* context) :
+    context_(context)
+    {
+    }
+
+    virtual void RestApiGet(std::string& result,
+                            const std::string& uri);
+
+    virtual void RestApiPost(std::string& result,
+                             const std::string& uri,
+                             const std::string& body);
+
+    virtual void RestApiPut(std::string& result,
+                            const std::string& uri,
+                            const std::string& body);
+
+    virtual void RestApiDelete(const std::string& uri);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,1939 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OrthancPluginCppWrapper.h"
+
+#include <json/reader.h>
+#include <json/writer.h>
+
+
+namespace OrthancPlugins
+{
+  void MemoryBuffer::Check(OrthancPluginErrorCode code)
+  {
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      // Prevent using garbage information
+      buffer_.data = NULL;
+      buffer_.size = 0;
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+
+
+  bool MemoryBuffer::CheckHttp(OrthancPluginErrorCode code)
+  {
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      // Prevent using garbage information
+      buffer_.data = NULL;
+      buffer_.size = 0;
+    }
+
+    if (code == OrthancPluginErrorCode_Success)
+    {
+      return true;
+    }
+    else if (code == OrthancPluginErrorCode_UnknownResource ||
+             code == OrthancPluginErrorCode_InexistentItem)
+    {
+      return false;
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+
+
+  MemoryBuffer::MemoryBuffer(OrthancPluginContext* context) :
+    context_(context)
+  {
+    buffer_.data = NULL;
+    buffer_.size = 0;
+  }
+
+
+  void MemoryBuffer::Clear()
+  {
+    if (buffer_.data != NULL)
+    {
+      OrthancPluginFreeMemoryBuffer(context_, &buffer_);
+      buffer_.data = NULL;
+      buffer_.size = 0;
+    }
+  }
+
+
+  void MemoryBuffer::Assign(OrthancPluginMemoryBuffer& other)
+  {
+    Clear();
+
+    buffer_.data = other.data;
+    buffer_.size = other.size;
+
+    other.data = NULL;
+    other.size = 0;
+  }
+
+
+  OrthancPluginMemoryBuffer MemoryBuffer::Release()
+  {
+    OrthancPluginMemoryBuffer result = buffer_;
+
+    buffer_.data = NULL;
+    buffer_.size = 0;
+    
+    return result;
+  }
+
+
+  void MemoryBuffer::ToString(std::string& target) const
+  {
+    if (buffer_.size == 0)
+    {
+      target.clear();
+    }
+    else
+    {
+      target.assign(reinterpret_cast<const char*>(buffer_.data), buffer_.size);
+    }
+  }
+
+
+  void MemoryBuffer::ToJson(Json::Value& target) const
+  {
+    if (buffer_.data == NULL ||
+        buffer_.size == 0)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+
+    const char* tmp = reinterpret_cast<const char*>(buffer_.data);
+
+    Json::Reader reader;
+    if (!reader.parse(tmp, tmp + buffer_.size, target))
+    {
+      OrthancPluginLogError(context_, "Cannot convert some memory buffer to JSON");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+  }
+
+
+  bool MemoryBuffer::RestApiGet(const std::string& uri,
+                                bool applyPlugins)
+  {
+    Clear();
+
+    if (applyPlugins)
+    {
+      return CheckHttp(OrthancPluginRestApiGetAfterPlugins(context_, &buffer_, uri.c_str()));
+    }
+    else
+    {
+      return CheckHttp(OrthancPluginRestApiGet(context_, &buffer_, uri.c_str()));
+    }
+  }
+
+  
+  bool MemoryBuffer::RestApiPost(const std::string& uri,
+                                 const char* body,
+                                 size_t bodySize,
+                                 bool applyPlugins)
+  {
+    Clear();
+
+    if (applyPlugins)
+    {
+      return CheckHttp(OrthancPluginRestApiPostAfterPlugins(context_, &buffer_, uri.c_str(), body, bodySize));
+    }
+    else
+    {
+      return CheckHttp(OrthancPluginRestApiPost(context_, &buffer_, uri.c_str(), body, bodySize));
+    }
+  }
+
+
+  bool MemoryBuffer::RestApiPut(const std::string& uri,
+                                const char* body,
+                                size_t bodySize,
+                                bool applyPlugins)
+  {
+    Clear();
+
+    if (applyPlugins)
+    {
+      return CheckHttp(OrthancPluginRestApiPutAfterPlugins(context_, &buffer_, uri.c_str(), body, bodySize));
+    }
+    else
+    {
+      return CheckHttp(OrthancPluginRestApiPut(context_, &buffer_, uri.c_str(), body, bodySize));
+    }
+  }
+
+
+  bool MemoryBuffer::RestApiPost(const std::string& uri,
+                                 const Json::Value& body,
+                                 bool applyPlugins)
+  {
+    Json::FastWriter writer;
+    return RestApiPost(uri, writer.write(body), applyPlugins);
+  }
+
+
+  bool MemoryBuffer::RestApiPut(const std::string& uri,
+                                const Json::Value& body,
+                                bool applyPlugins)
+  {
+    Json::FastWriter writer;
+    return RestApiPut(uri, writer.write(body), applyPlugins);
+  }
+
+
+  void MemoryBuffer::CreateDicom(const Json::Value& tags,
+                                 OrthancPluginCreateDicomFlags flags)
+  {
+    Clear();
+
+    Json::FastWriter writer;
+    std::string s = writer.write(tags);
+    
+    Check(OrthancPluginCreateDicom(context_, &buffer_, s.c_str(), NULL, flags));
+  }
+
+  void MemoryBuffer::CreateDicom(const Json::Value& tags,
+                                 const OrthancImage& pixelData,
+                                 OrthancPluginCreateDicomFlags flags)
+  {
+    Clear();
+
+    Json::FastWriter writer;
+    std::string s = writer.write(tags);
+
+    Check(OrthancPluginCreateDicom(context_, &buffer_, s.c_str(), pixelData.GetObject(), flags));
+  }
+
+
+  void MemoryBuffer::ReadFile(const std::string& path)
+  {
+    Clear();
+    Check(OrthancPluginReadFile(context_, &buffer_, path.c_str()));
+  }
+
+
+  void MemoryBuffer::GetDicomQuery(const OrthancPluginWorklistQuery* query)
+  {
+    Clear();
+    Check(OrthancPluginWorklistGetDicomQuery(context_, &buffer_, query));
+  }
+
+
+  void OrthancString::Assign(char* str)
+  {
+    if (str == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+    else
+    {
+      Clear();
+      str_ = str;
+    }
+  }
+
+
+  void OrthancString::Clear()
+  {
+    if (str_ != NULL)
+    {
+      OrthancPluginFreeString(context_, str_);
+      str_ = NULL;
+    }
+  }
+
+
+  void OrthancString::ToString(std::string& target) const
+  {
+    if (str_ == NULL)
+    {
+      target.clear();
+    }
+    else
+    {
+      target.assign(str_);
+    }
+  }
+
+
+  void OrthancString::ToJson(Json::Value& target) const
+  {
+    if (str_ == NULL)
+    {
+      OrthancPluginLogError(context_, "Cannot convert an empty memory buffer to JSON");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+
+    Json::Reader reader;
+    if (!reader.parse(str_, target))
+    {
+      OrthancPluginLogError(context_, "Cannot convert some memory buffer to JSON");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+  }
+
+  
+  void MemoryBuffer::DicomToJson(Json::Value& target,
+                                 OrthancPluginDicomToJsonFormat format,
+                                 OrthancPluginDicomToJsonFlags flags,
+                                 uint32_t maxStringLength)
+  {
+    OrthancString str(context_);
+    str.Assign(OrthancPluginDicomBufferToJson(context_, GetData(), GetSize(), format, flags, maxStringLength));
+    str.ToJson(target);
+  }
+
+
+  bool MemoryBuffer::HttpGet(const std::string& url,
+                             const std::string& username,
+                             const std::string& password)
+  {
+    Clear();
+    return CheckHttp(OrthancPluginHttpGet(context_, &buffer_, url.c_str(),
+                                          username.empty() ? NULL : username.c_str(),
+                                          password.empty() ? NULL : password.c_str()));
+  }
+
+  
+  bool MemoryBuffer::HttpPost(const std::string& url,
+                              const std::string& body,
+                              const std::string& username,
+                              const std::string& password)
+  {
+    Clear();
+    return CheckHttp(OrthancPluginHttpPost(context_, &buffer_, url.c_str(),
+                                           body.c_str(), body.size(),
+                                           username.empty() ? NULL : username.c_str(),
+                                           password.empty() ? NULL : password.c_str()));
+  }
+  
+
+  bool MemoryBuffer::HttpPut(const std::string& url,
+                             const std::string& body,
+                             const std::string& username,
+                             const std::string& password)
+  {
+    Clear();
+    return CheckHttp(OrthancPluginHttpPut(context_, &buffer_, url.c_str(),
+                                          body.empty() ? NULL : body.c_str(),
+                                          body.size(),
+                                          username.empty() ? NULL : username.c_str(),
+                                          password.empty() ? NULL : password.c_str()));
+  }
+  
+
+  void MemoryBuffer::GetDicomInstance(const std::string& instanceId)
+  {
+    Clear();
+    Check(OrthancPluginGetDicomForInstance(context_, &buffer_, instanceId.c_str()));
+  }
+
+  
+  bool HttpDelete(OrthancPluginContext* context_,
+                  const std::string& url,
+                  const std::string& username,
+                  const std::string& password)
+  {
+    OrthancPluginErrorCode error = OrthancPluginHttpDelete
+        (context_, url.c_str(),
+         username.empty() ? NULL : username.c_str(),
+         password.empty() ? NULL : password.c_str());
+
+    if (error == OrthancPluginErrorCode_Success)
+    {
+      return true;
+    }
+    else if (error == OrthancPluginErrorCode_UnknownResource ||
+             error == OrthancPluginErrorCode_InexistentItem)
+    {
+      return false;
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
+    }
+  }
+  
+
+  OrthancConfiguration::OrthancConfiguration(OrthancPluginContext* context) :
+    context_(context)
+  {
+    OrthancString str(context);
+    str.Assign(OrthancPluginGetConfiguration(context));
+
+    if (str.GetContent() == NULL)
+    {
+      OrthancPluginLogError(context, "Cannot access the Orthanc configuration");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+
+    str.ToJson(configuration_);
+
+    if (configuration_.type() != Json::objectValue)
+    {
+      OrthancPluginLogError(context, "Unable to read the Orthanc configuration");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+  }
+
+
+  OrthancPluginContext* OrthancConfiguration::GetContext() const
+  {
+    if (context_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
+    }
+    else
+    {
+      return context_;
+    }
+  }
+
+
+  std::string OrthancConfiguration::GetPath(const std::string& key) const
+  {
+    if (path_.empty())
+    {
+      return key;
+    }
+    else
+    {
+      return path_ + "." + key;
+    }
+  }
+
+
+  bool OrthancConfiguration::IsSection(const std::string& key) const
+  {
+    assert(configuration_.type() == Json::objectValue);
+
+    return (configuration_.isMember(key) &&
+            configuration_[key].type() == Json::objectValue);
+  }
+
+
+  void OrthancConfiguration::GetSection(OrthancConfiguration& target,
+                                        const std::string& key) const
+  {
+    assert(configuration_.type() == Json::objectValue);
+
+    target.context_ = context_;
+    target.path_ = GetPath(key);
+
+    if (!configuration_.isMember(key))
+    {
+      target.configuration_ = Json::objectValue;
+    }
+    else
+    {
+      if (configuration_[key].type() != Json::objectValue)
+      {
+        if (context_ != NULL)
+        {
+          std::string s = "The configuration section \"" + target.path_ + "\" is not an associative array as expected";
+          OrthancPluginLogError(context_, s.c_str());
+        }
+
+        ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+      }
+
+      target.configuration_ = configuration_[key];
+    }
+  }
+
+
+  bool OrthancConfiguration::LookupStringValue(std::string& target,
+                                               const std::string& key) const
+  {
+    assert(configuration_.type() == Json::objectValue);
+
+    if (!configuration_.isMember(key))
+    {
+      return false;
+    }
+
+    if (configuration_[key].type() != Json::stringValue)
+    {
+      if (context_ != NULL)
+      {
+        std::string s = "The configuration option \"" + GetPath(key) + "\" is not a string as expected";
+        OrthancPluginLogError(context_, s.c_str());
+      }
+
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+
+    target = configuration_[key].asString();
+    return true;
+  }
+
+
+  bool OrthancConfiguration::LookupIntegerValue(int& target,
+                                                const std::string& key) const
+  {
+    assert(configuration_.type() == Json::objectValue);
+
+    if (!configuration_.isMember(key))
+    {
+      return false;
+    }
+
+    switch (configuration_[key].type())
+    {
+    case Json::intValue:
+      target = configuration_[key].asInt();
+      return true;
+
+    case Json::uintValue:
+      target = configuration_[key].asUInt();
+      return true;
+
+    default:
+      if (context_ != NULL)
+      {
+        std::string s = "The configuration option \"" + GetPath(key) + "\" is not an integer as expected";
+        OrthancPluginLogError(context_, s.c_str());
+      }
+
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+  }
+
+
+  bool OrthancConfiguration::LookupUnsignedIntegerValue(unsigned int& target,
+                                                        const std::string& key) const
+  {
+    int tmp;
+    if (!LookupIntegerValue(tmp, key))
+    {
+      return false;
+    }
+
+    if (tmp < 0)
+    {
+      if (context_ != NULL)
+      {
+        std::string s = "The configuration option \"" + GetPath(key) + "\" is not a positive integer as expected";
+        OrthancPluginLogError(context_, s.c_str());
+      }
+
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+    else
+    {
+      target = static_cast<unsigned int>(tmp);
+      return true;
+    }
+  }
+
+
+  bool OrthancConfiguration::LookupBooleanValue(bool& target,
+                                                const std::string& key) const
+  {
+    assert(configuration_.type() == Json::objectValue);
+
+    if (!configuration_.isMember(key))
+    {
+      return false;
+    }
+
+    if (configuration_[key].type() != Json::booleanValue)
+    {
+      if (context_ != NULL)
+      {
+        std::string s = "The configuration option \"" + GetPath(key) + "\" is not a Boolean as expected";
+        OrthancPluginLogError(context_, s.c_str());
+      }
+
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+
+    target = configuration_[key].asBool();
+    return true;
+  }
+
+
+  bool OrthancConfiguration::LookupFloatValue(float& target,
+                                              const std::string& key) const
+  {
+    assert(configuration_.type() == Json::objectValue);
+
+    if (!configuration_.isMember(key))
+    {
+      return false;
+    }
+
+    switch (configuration_[key].type())
+    {
+    case Json::realValue:
+      target = configuration_[key].asFloat();
+      return true;
+
+    case Json::intValue:
+      target = static_cast<float>(configuration_[key].asInt());
+      return true;
+
+    case Json::uintValue:
+      target = static_cast<float>(configuration_[key].asUInt());
+      return true;
+
+    default:
+      if (context_ != NULL)
+      {
+        std::string s = "The configuration option \"" + GetPath(key) + "\" is not an integer as expected";
+        OrthancPluginLogError(context_, s.c_str());
+      }
+
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+  }
+
+
+  bool OrthancConfiguration::LookupListOfStrings(std::list<std::string>& target,
+                                                 const std::string& key,
+                                                 bool allowSingleString) const
+  {
+    assert(configuration_.type() == Json::objectValue);
+
+    target.clear();
+
+    if (!configuration_.isMember(key))
+    {
+      return false;
+    }
+
+    switch (configuration_[key].type())
+    {
+    case Json::arrayValue:
+    {
+      bool ok = true;
+
+      for (Json::Value::ArrayIndex i = 0; ok && i < configuration_[key].size(); i++)
+      {
+        if (configuration_[key][i].type() == Json::stringValue)
+        {
+          target.push_back(configuration_[key][i].asString());
+        }
+        else
+        {
+          ok = false;
+        }
+      }
+
+      if (ok)
+      {
+        return true;
+      }
+
+      break;
+    }
+
+    case Json::stringValue:
+      if (allowSingleString)
+      {
+        target.push_back(configuration_[key].asString());
+        return true;
+      }
+
+      break;
+
+    default:
+      break;
+    }
+
+    if (context_ != NULL)
+    {
+      std::string s = ("The configuration option \"" + GetPath(key) +
+                       "\" is not a list of strings as expected");
+      OrthancPluginLogError(context_, s.c_str());
+    }
+
+    ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+  }
+
+
+  bool OrthancConfiguration::LookupSetOfStrings(std::set<std::string>& target,
+                                                const std::string& key,
+                                                bool allowSingleString) const
+  {
+    std::list<std::string> lst;
+
+    if (LookupListOfStrings(lst, key, allowSingleString))
+    {
+      target.clear();
+
+      for (std::list<std::string>::const_iterator
+           it = lst.begin(); it != lst.end(); ++it)
+      {
+        target.insert(*it);
+      }
+
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  
+  std::string OrthancConfiguration::GetStringValue(const std::string& key,
+                                                   const std::string& defaultValue) const
+  {
+    std::string tmp;
+    if (LookupStringValue(tmp, key))
+    {
+      return tmp;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  int OrthancConfiguration::GetIntegerValue(const std::string& key,
+                                            int defaultValue) const
+  {
+    int tmp;
+    if (LookupIntegerValue(tmp, key))
+    {
+      return tmp;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  unsigned int OrthancConfiguration::GetUnsignedIntegerValue(const std::string& key,
+                                                             unsigned int defaultValue) const
+  {
+    unsigned int tmp;
+    if (LookupUnsignedIntegerValue(tmp, key))
+    {
+      return tmp;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  bool OrthancConfiguration::GetBooleanValue(const std::string& key,
+                                             bool defaultValue) const
+  {
+    bool tmp;
+    if (LookupBooleanValue(tmp, key))
+    {
+      return tmp;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  float OrthancConfiguration::GetFloatValue(const std::string& key,
+                                            float defaultValue) const
+  {
+    float tmp;
+    if (LookupFloatValue(tmp, key))
+    {
+      return tmp;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  void OrthancConfiguration::GetDictionary(std::map<std::string, std::string>& target,
+                                           const std::string& key) const
+  {
+    assert(configuration_.type() == Json::objectValue);
+
+    target.clear();
+
+    if (!configuration_.isMember(key))
+    {
+      return;
+    }
+
+    if (configuration_[key].type() != Json::objectValue)
+    {
+      if (context_ != NULL)
+      {
+        std::string s = "The configuration option \"" + GetPath(key) + "\" is not a string as expected";
+        OrthancPluginLogError(context_, s.c_str());
+      }
+
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+
+    Json::Value::Members members = configuration_[key].getMemberNames();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const Json::Value& value = configuration_[key][members[i]];
+
+      if (value.type() == Json::stringValue)
+      {
+        target[members[i]] = value.asString();
+      }
+      else
+      {
+        if (context_ != NULL)
+        {
+          std::string s = "The configuration option \"" + GetPath(key) + "\" is not a dictionary mapping strings to strings";
+          OrthancPluginLogError(context_, s.c_str());
+        }
+
+        ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+      }
+    }
+  }
+
+
+  void OrthancImage::Clear()
+  {
+    if (image_ != NULL)
+    {
+      OrthancPluginFreeImage(context_, image_);
+      image_ = NULL;
+    }
+  }
+
+
+  void OrthancImage::CheckImageAvailable()
+  {
+    if (image_ == NULL)
+    {
+      OrthancPluginLogError(context_, "Trying to access a NULL image");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+  }
+
+
+  OrthancImage::OrthancImage(OrthancPluginContext*  context) :
+    context_(context),
+    image_(NULL)
+  {
+    if (context == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+  }
+
+
+  OrthancImage::OrthancImage(OrthancPluginContext*  context,
+                             OrthancPluginImage*    image) :
+    context_(context),
+    image_(image)
+  {
+    if (context == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+  }
+  
+
+  OrthancImage::OrthancImage(OrthancPluginContext*     context,
+                             OrthancPluginPixelFormat  format,
+                             uint32_t                  width,
+                             uint32_t                  height) :
+    context_(context)
+  {
+    if (context == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+    else
+    {
+      image_ = OrthancPluginCreateImage(context, format, width, height);
+    }
+  }
+
+  OrthancImage::OrthancImage(OrthancPluginContext*     context,
+                             OrthancPluginPixelFormat  format,
+                             uint32_t                  width,
+                             uint32_t                  height,
+                             uint32_t                  pitch,
+                             void*                     buffer) :
+    context_(context)
+  {
+    if (context == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+    else
+    {
+      image_ = OrthancPluginCreateImageAccessor(context, format, width, height, pitch, buffer);
+    }
+  }
+
+  void OrthancImage::UncompressPngImage(const void* data,
+                                        size_t size)
+  {
+    Clear();
+    image_ = OrthancPluginUncompressImage(context_, data, size, OrthancPluginImageFormat_Png);
+    if (image_ == NULL)
+    {
+      OrthancPluginLogError(context_, "Cannot uncompress a PNG image");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+  }
+
+
+  void OrthancImage::UncompressJpegImage(const void* data,
+                                         size_t size)
+  {
+    Clear();
+    image_ = OrthancPluginUncompressImage(context_, data, size, OrthancPluginImageFormat_Jpeg);
+    if (image_ == NULL)
+    {
+      OrthancPluginLogError(context_, "Cannot uncompress a JPEG image");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+  }
+
+
+  void OrthancImage::DecodeDicomImage(const void* data,
+                                      size_t size,
+                                      unsigned int frame)
+  {
+    Clear();
+    image_ = OrthancPluginDecodeDicomImage(context_, data, size, frame);
+    if (image_ == NULL)
+    {
+      OrthancPluginLogError(context_, "Cannot uncompress a DICOM image");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+  }
+
+
+  OrthancPluginPixelFormat OrthancImage::GetPixelFormat()
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImagePixelFormat(context_, image_);
+  }
+
+
+  unsigned int OrthancImage::GetWidth()
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImageWidth(context_, image_);
+  }
+
+
+  unsigned int OrthancImage::GetHeight()
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImageHeight(context_, image_);
+  }
+
+
+  unsigned int OrthancImage::GetPitch()
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImagePitch(context_, image_);
+  }
+
+
+  const void* OrthancImage::GetBuffer()
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImageBuffer(context_, image_);
+  }
+
+
+  void OrthancImage::CompressPngImage(MemoryBuffer& target)
+  {
+    CheckImageAvailable();
+    
+    OrthancPluginMemoryBuffer tmp;
+    OrthancPluginCompressPngImage(context_, &tmp, GetPixelFormat(),
+                                  GetWidth(), GetHeight(), GetPitch(), GetBuffer());
+
+    target.Assign(tmp);
+  }
+
+
+  void OrthancImage::CompressJpegImage(MemoryBuffer& target,
+                                       uint8_t quality)
+  {
+    CheckImageAvailable();
+    
+    OrthancPluginMemoryBuffer tmp;
+    OrthancPluginCompressJpegImage(context_, &tmp, GetPixelFormat(),
+                                   GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality);
+    
+    target.Assign(tmp);
+  }
+
+
+  void OrthancImage::AnswerPngImage(OrthancPluginRestOutput* output)
+  {
+    CheckImageAvailable();
+    OrthancPluginCompressAndAnswerPngImage(context_, output, GetPixelFormat(),
+                                           GetWidth(), GetHeight(), GetPitch(), GetBuffer());
+  }
+
+
+  void OrthancImage::AnswerJpegImage(OrthancPluginRestOutput* output,
+                                     uint8_t quality)
+  {
+    CheckImageAvailable();
+    OrthancPluginCompressAndAnswerJpegImage(context_, output, GetPixelFormat(),
+                                            GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality);
+  }
+
+
+
+#if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1
+  FindMatcher::FindMatcher(OrthancPluginContext*              context,
+                           const OrthancPluginWorklistQuery*  worklist) :
+    context_(context),
+    matcher_(NULL),
+    worklist_(worklist)
+  {
+    if (worklist_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+  }
+
+
+  void FindMatcher::SetupDicom(OrthancPluginContext*  context,
+                               const void*            query,
+                               uint32_t               size)
+  {
+    context_ = context;
+    worklist_ = NULL;
+
+    matcher_ = OrthancPluginCreateFindMatcher(context_, query, size);
+    if (matcher_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+  }
+
+
+  FindMatcher::~FindMatcher()
+  {
+    // The "worklist_" field
+
+    if (matcher_ != NULL)
+    {
+      OrthancPluginFreeFindMatcher(context_, matcher_);
+    }
+  }
+
+
+
+  bool FindMatcher::IsMatch(const void*  dicom,
+                            uint32_t     size) const
+  {
+    int32_t result;
+
+    if (matcher_ != NULL)
+    {
+      result = OrthancPluginFindMatcherIsMatch(context_, matcher_, dicom, size);
+    }
+    else if (worklist_ != NULL)
+    {
+      result = OrthancPluginWorklistIsMatch(context_, worklist_, dicom, size);
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+
+    if (result == 0)
+    {
+      return false;
+    }
+    else if (result == 1)
+    {
+      return true;
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+  }
+
+#endif /* HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 */
+
+
+  bool RestApiGet(Json::Value& result,
+                  OrthancPluginContext* context,
+                  const std::string& uri,
+                  bool applyPlugins)
+  {
+    MemoryBuffer answer(context);
+    if (!answer.RestApiGet(uri, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      answer.ToJson(result);
+      return true;
+    }
+  }
+
+
+  bool RestApiPost(Json::Value& result,
+                   OrthancPluginContext* context,
+                   const std::string& uri,
+                   const char* body,
+                   size_t bodySize,
+                   bool applyPlugins)
+  {
+    MemoryBuffer answer(context);
+    if (!answer.RestApiPost(uri, body, bodySize, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      answer.ToJson(result);
+      return true;
+    }
+  }
+
+
+  bool RestApiPost(Json::Value& result,
+                   OrthancPluginContext* context,
+                   const std::string& uri,
+                   const Json::Value& body,
+                   bool applyPlugins)
+  {
+    Json::FastWriter writer;
+    return RestApiPost(result, context, uri, writer.write(body), applyPlugins);
+  }
+
+
+  bool RestApiPut(Json::Value& result,
+                  OrthancPluginContext* context,
+                  const std::string& uri,
+                  const char* body,
+                  size_t bodySize,
+                  bool applyPlugins)
+  {
+    MemoryBuffer answer(context);
+    if (!answer.RestApiPut(uri, body, bodySize, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      answer.ToJson(result);
+      return true;
+    }
+  }
+
+
+  bool RestApiPut(Json::Value& result,
+                  OrthancPluginContext* context,
+                  const std::string& uri,
+                  const Json::Value& body,
+                  bool applyPlugins)
+  {
+    Json::FastWriter writer;
+    return RestApiPut(result, context, uri, writer.write(body), applyPlugins);
+  }
+
+
+  bool RestApiDelete(OrthancPluginContext* context,
+                     const std::string& uri,
+                     bool applyPlugins)
+  {
+    OrthancPluginErrorCode error;
+
+    if (applyPlugins)
+    {
+      error = OrthancPluginRestApiDeleteAfterPlugins(context, uri.c_str());
+    }
+    else
+    {
+      error = OrthancPluginRestApiDelete(context, uri.c_str());
+    }
+
+    if (error == OrthancPluginErrorCode_Success)
+    {
+      return true;
+    }
+    else if (error == OrthancPluginErrorCode_UnknownResource ||
+             error == OrthancPluginErrorCode_InexistentItem)
+    {
+      return false;
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
+    }
+  }
+
+
+  void ReportMinimalOrthancVersion(OrthancPluginContext* context,
+                                   unsigned int major,
+                                   unsigned int minor,
+                                   unsigned int revision)
+  {
+    std::string s = ("Your version of the Orthanc core (" +
+                     std::string(context->orthancVersion) +
+                     ") is too old to run this plugin (version " +
+                     boost::lexical_cast<std::string>(major) + "." +
+                     boost::lexical_cast<std::string>(minor) + "." +
+                     boost::lexical_cast<std::string>(revision) +
+                     " is required)");
+    
+    OrthancPluginLogError(context, s.c_str());
+  }
+
+
+  bool CheckMinimalOrthancVersion(OrthancPluginContext* context,
+                                  unsigned int major,
+                                  unsigned int minor,
+                                  unsigned int revision)
+  {
+    if (context == NULL)
+    {
+      OrthancPluginLogError(context, "Bad Orthanc context in the plugin");
+      return false;
+    }
+
+    if (!strcmp(context->orthancVersion, "mainline"))
+    {
+      // Assume compatibility with the mainline
+      return true;
+    }
+
+    // Parse the version of the Orthanc core
+    int aa, bb, cc;
+    if (
+    #ifdef _MSC_VER
+        sscanf_s
+    #else
+        sscanf
+    #endif
+        (context->orthancVersion, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 ||
+        aa < 0 ||
+        bb < 0 ||
+        cc < 0)
+    {
+      return false;
+    }
+
+    unsigned int a = static_cast<unsigned int>(aa);
+    unsigned int b = static_cast<unsigned int>(bb);
+    unsigned int c = static_cast<unsigned int>(cc);
+
+    // Check the major version number
+
+    if (a > major)
+    {
+      return true;
+    }
+
+    if (a < major)
+    {
+      return false;
+    }
+
+
+    // Check the minor version number
+    assert(a == major);
+
+    if (b > minor)
+    {
+      return true;
+    }
+
+    if (b < minor)
+    {
+      return false;
+    }
+
+    // Check the patch level version number
+    assert(a == major && b == minor);
+
+    if (c >= revision)
+    {
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+
+
+#if HAS_ORTHANC_PLUGIN_PEERS == 1
+  size_t OrthancPeers::GetPeerIndex(const std::string& name) const
+  {
+    size_t index;
+    if (LookupName(index, name))
+    {
+      return index;
+    }
+    else
+    {
+      std::string s = "Inexistent peer: " + name;
+      OrthancPluginLogError(context_, s.c_str());
+      ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource);
+    }
+  }
+
+
+  OrthancPeers::OrthancPeers(OrthancPluginContext* context) :
+    context_(context),
+    peers_(NULL),
+    timeout_(0)
+  {
+    if (context_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer);
+    }
+
+    peers_ = OrthancPluginGetPeers(context_);
+
+    if (peers_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+    }
+
+    uint32_t count = OrthancPluginGetPeersCount(context_, peers_);
+
+    for (uint32_t i = 0; i < count; i++)
+    {
+      const char* name = OrthancPluginGetPeerName(context_, peers_, i);
+      if (name == NULL)
+      {
+        OrthancPluginFreePeers(context_, peers_);
+        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+      }
+
+      index_[name] = i;
+    }
+  }
+
+  
+  OrthancPeers::~OrthancPeers()
+  {
+    if (peers_ != NULL)
+    {
+      OrthancPluginFreePeers(context_, peers_);
+    }
+  }
+
+  
+  bool OrthancPeers::LookupName(size_t& target,
+                                const std::string& name) const
+  {
+    Index::const_iterator found = index_.find(name);
+
+    if (found == index_.end())
+    {
+      return false;
+    }
+    else
+    {
+      target = found->second;
+      return true;
+    }
+  }
+
+  
+  std::string OrthancPeers::GetPeerName(size_t index) const
+  {
+    if (index >= index_.size())
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      const char* s = OrthancPluginGetPeerName(context_, peers_, static_cast<uint32_t>(index));
+      if (s == NULL)
+      {
+        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+      }
+      else
+      {
+        return s;
+      }
+    }
+  }
+  
+
+  std::string OrthancPeers::GetPeerUrl(size_t index) const
+  {
+    if (index >= index_.size())
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      const char* s = OrthancPluginGetPeerUrl(context_, peers_, static_cast<uint32_t>(index));
+      if (s == NULL)
+      {
+        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+      }
+      else
+      {
+        return s;
+      }
+    }
+  }
+
+  
+  std::string OrthancPeers::GetPeerUrl(const std::string& name) const
+  {
+    return GetPeerUrl(GetPeerIndex(name));
+  }
+
+
+  bool OrthancPeers::LookupUserProperty(std::string& value,
+                                        size_t index,
+                                        const std::string& key) const
+  {
+    if (index >= index_.size())
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      const char* s = OrthancPluginGetPeerUserProperty(context_, peers_, static_cast<uint32_t>(index), key.c_str());
+      if (s == NULL)
+      {
+        return false;
+      }
+      else
+      {
+        value.assign(s);
+        return true;
+      }
+    }
+  }
+
+
+  bool OrthancPeers::LookupUserProperty(std::string& value,
+                                        const std::string& peer,
+                                        const std::string& key) const
+  {
+    return LookupUserProperty(value, GetPeerIndex(peer), key);
+  }
+
+
+  bool OrthancPeers::DoGet(MemoryBuffer& target,
+                           size_t index,
+                           const std::string& uri) const
+  {
+    if (index >= index_.size())
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+
+    OrthancPluginMemoryBuffer answer;
+    uint16_t status;
+    OrthancPluginErrorCode code = OrthancPluginCallPeerApi
+        (context_, &answer, NULL, &status, peers_,
+         static_cast<uint32_t>(index), OrthancPluginHttpMethod_Get, uri.c_str(),
+         0, NULL, NULL, NULL, 0, timeout_);
+
+    if (code == OrthancPluginErrorCode_Success)
+    {
+      target.Assign(answer);
+      return (status == 200);
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  
+  bool OrthancPeers::DoGet(MemoryBuffer& target,
+                           const std::string& name,
+                           const std::string& uri) const
+  {
+    size_t index;
+    return (LookupName(index, name) &&
+            DoGet(target, index, uri));
+  }
+  
+
+  bool OrthancPeers::DoGet(Json::Value& target,
+                           size_t index,
+                           const std::string& uri) const
+  {
+    MemoryBuffer buffer(context_);
+
+    if (DoGet(buffer, index, uri))
+    {
+      buffer.ToJson(target);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+
+  bool OrthancPeers::DoGet(Json::Value& target,
+                           const std::string& name,
+                           const std::string& uri) const
+  {
+    MemoryBuffer buffer(context_);
+
+    if (DoGet(buffer, name, uri))
+    {
+      buffer.ToJson(target);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  
+  bool OrthancPeers::DoPost(MemoryBuffer& target,
+                            const std::string& name,
+                            const std::string& uri,
+                            const std::string& body) const
+  {
+    size_t index;
+    return (LookupName(index, name) &&
+            DoPost(target, index, uri, body));
+  }
+  
+
+  bool OrthancPeers::DoPost(Json::Value& target,
+                            size_t index,
+                            const std::string& uri,
+                            const std::string& body) const
+  {
+    MemoryBuffer buffer(context_);
+
+    if (DoPost(buffer, index, uri, body))
+    {
+      buffer.ToJson(target);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+
+  bool OrthancPeers::DoPost(Json::Value& target,
+                            const std::string& name,
+                            const std::string& uri,
+                            const std::string& body) const
+  {
+    MemoryBuffer buffer(context_);
+
+    if (DoPost(buffer, name, uri, body))
+    {
+      buffer.ToJson(target);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  
+  bool OrthancPeers::DoPost(MemoryBuffer& target,
+                            size_t index,
+                            const std::string& uri,
+                            const std::string& body) const
+  {
+    if (index >= index_.size())
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+
+    OrthancPluginMemoryBuffer answer;
+    uint16_t status;
+    OrthancPluginErrorCode code = OrthancPluginCallPeerApi
+        (context_, &answer, NULL, &status, peers_,
+         static_cast<uint32_t>(index), OrthancPluginHttpMethod_Post, uri.c_str(),
+         0, NULL, NULL, body.empty() ? NULL : body.c_str(), body.size(), timeout_);
+
+    if (code == OrthancPluginErrorCode_Success)
+    {
+      target.Assign(answer);
+      return (status == 200);
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+
+  bool OrthancPeers::DoPut(size_t index,
+                           const std::string& uri,
+                           const std::string& body) const
+  {
+    if (index >= index_.size())
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+
+    OrthancPluginMemoryBuffer answer;
+    uint16_t status;
+    OrthancPluginErrorCode code = OrthancPluginCallPeerApi
+        (context_, &answer, NULL, &status, peers_,
+         static_cast<uint32_t>(index), OrthancPluginHttpMethod_Put, uri.c_str(),
+         0, NULL, NULL, body.empty() ? NULL : body.c_str(), body.size(), timeout_);
+
+    if (code == OrthancPluginErrorCode_Success)
+    {
+      OrthancPluginFreeMemoryBuffer(context_, &answer);
+      return (status == 200);
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+
+  bool OrthancPeers::DoPut(const std::string& name,
+                           const std::string& uri,
+                           const std::string& body) const
+  {
+    size_t index;
+    return (LookupName(index, name) &&
+            DoPut(index, uri, body));
+  }
+  
+
+  bool OrthancPeers::DoDelete(size_t index,
+                              const std::string& uri) const
+  {
+    if (index >= index_.size())
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+
+    OrthancPluginMemoryBuffer answer;
+    uint16_t status;
+    OrthancPluginErrorCode code = OrthancPluginCallPeerApi
+        (context_, &answer, NULL, &status, peers_,
+         static_cast<uint32_t>(index), OrthancPluginHttpMethod_Put, uri.c_str(),
+         0, NULL, NULL, NULL, 0, timeout_);
+
+    if (code == OrthancPluginErrorCode_Success)
+    {
+      OrthancPluginFreeMemoryBuffer(context_, &answer);
+      return (status == 200);
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  
+  bool OrthancPeers::DoDelete(const std::string& name,
+                              const std::string& uri) const
+  {
+    size_t index;
+    return (LookupName(index, name) &&
+            DoDelete(index, uri));
+  }
+#endif
+
+
+  
+#if HAS_ORTHANC_PLUGIN_JOB == 1
+  void OrthancJob::CallbackFinalize(void* job)
+  {
+    if (job != NULL)
+    {
+      delete reinterpret_cast<OrthancJob*>(job);
+    }
+  }
+  
+
+  float OrthancJob::CallbackGetProgress(void* job)
+  {
+    assert(job != NULL);
+
+    try
+    {
+      return reinterpret_cast<OrthancJob*>(job)->progress_;
+    }
+    catch (...)
+    {
+      return 0;
+    }
+  }
+  
+
+  const char* OrthancJob::CallbackGetContent(void* job)
+  {
+    assert(job != NULL);
+
+    try
+    {
+      return reinterpret_cast<OrthancJob*>(job)->content_.c_str();
+    }
+    catch (...)
+    {
+      return 0;
+    }
+  }
+  
+
+  const char* OrthancJob::CallbackGetSerialized(void* job)
+  {
+    assert(job != NULL);
+
+    try
+    {
+      const OrthancJob& tmp = *reinterpret_cast<OrthancJob*>(job);
+
+      if (tmp.hasSerialized_)
+      {
+        return tmp.serialized_.c_str();
+      }
+      else
+      {
+        return NULL;
+      }
+    }
+    catch (...)
+    {
+      return 0;
+    }
+  }
+  
+
+  OrthancPluginJobStepStatus OrthancJob::CallbackStep(void* job)
+  {
+    assert(job != NULL);
+
+    try
+    {
+      return reinterpret_cast<OrthancJob*>(job)->Step();
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&)
+    {
+      return OrthancPluginJobStepStatus_Failure;
+    }
+    catch (...)
+    {
+      return OrthancPluginJobStepStatus_Failure;
+    }
+  }
+
+  
+  OrthancPluginErrorCode OrthancJob::CallbackStop(void* job,
+                                                  OrthancPluginJobStopReason reason)
+  {
+    assert(job != NULL);
+
+    try
+    {
+      reinterpret_cast<OrthancJob*>(job)->Stop(reason);
+      return OrthancPluginErrorCode_Success;
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_Plugin;
+    }
+  }
+  
+
+  OrthancPluginErrorCode OrthancJob::CallbackReset(void* job)
+  {
+    assert(job != NULL);
+
+    try
+    {
+      reinterpret_cast<OrthancJob*>(job)->Reset();
+      return OrthancPluginErrorCode_Success;
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_Plugin;
+    }
+  }
+  
+
+  void OrthancJob::ClearContent()
+  {
+    Json::Value empty = Json::objectValue;
+    UpdateContent(empty);
+  }
+
+  
+  void OrthancJob::UpdateContent(const Json::Value& content)
+  {
+    if (content.type() != Json::objectValue)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_BadFileFormat);
+    }
+    else
+    {
+      Json::FastWriter writer;
+      content_ = writer.write(content);
+    }
+  }
+  
+
+  void OrthancJob::ClearSerialized()
+  {
+    hasSerialized_ = false;
+    serialized_.clear();
+  }
+
+  
+  void OrthancJob::UpdateSerialized(const Json::Value& serialized)
+  {
+    if (serialized.type() != Json::objectValue)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_BadFileFormat);
+    }
+    else
+    {
+      Json::FastWriter writer;
+      serialized_ = writer.write(serialized);
+      hasSerialized_ = true;
+    }
+  }
+
+  
+  void OrthancJob::UpdateProgress(float progress)
+  {
+    if (progress < 0 ||
+        progress > 1)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+
+    progress_ = progress;
+  }
+
+
+  OrthancJob::OrthancJob(const std::string& jobType) :
+    jobType_(jobType),
+    progress_(0)
+  {
+    ClearContent();
+    ClearSerialized();
+  }
+
+
+  OrthancPluginJob* OrthancJob::Create(OrthancPluginContext* context,
+                                       OrthancJob* job)
+  {
+    if (job == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer);
+    }
+
+    OrthancPluginJob* orthanc = OrthancPluginCreateJob(
+          context, job, CallbackFinalize, job->jobType_.c_str(),
+          CallbackGetProgress, CallbackGetContent, CallbackGetSerialized,
+          CallbackStep, CallbackStop, CallbackReset);
+
+    if (orthanc == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+    }
+    else
+    {
+      return orthanc;
+    }
+  }
+
+  
+  std::string OrthancJob::Submit(OrthancPluginContext* context,
+                                 OrthancJob* job,
+                                 int priority)
+  {
+    OrthancPluginJob* orthanc = Create(context, job);
+    
+    char* id = OrthancPluginSubmitJob(context, orthanc, priority);
+
+    if (id == NULL)
+    {
+      OrthancPluginLogError(context, "Plugin cannot submit job");
+      OrthancPluginFreeJob(context, orthanc);
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+    }
+    else
+    {
+      std::string tmp(id);
+      tmp.assign(id);
+      OrthancPluginFreeString(context, id);
+
+      return tmp;
+    }
+  }
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,726 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "OrthancPluginException.h"
+
+#include <orthanc/OrthancCPlugin.h>
+#include <boost/noncopyable.hpp>
+#include <boost/lexical_cast.hpp>
+#include <json/value.h>
+#include <list>
+#include <set>
+#include <map>
+
+
+
+#if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
+#define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision)        \
+  (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major ||                      \
+   (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major &&                    \
+    (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor ||                    \
+     (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor &&                  \
+      ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision))))
+#endif
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 2, 0)
+// The "OrthancPluginFindMatcher()" primitive was introduced in Orthanc 1.2.0
+#  define HAS_ORTHANC_PLUGIN_FIND_MATCHER  1
+#else
+#  define HAS_ORTHANC_PLUGIN_FIND_MATCHER  0
+#endif
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 4, 2)
+#  define HAS_ORTHANC_PLUGIN_PEERS  1
+#  define HAS_ORTHANC_PLUGIN_JOB    1
+#else
+#  define HAS_ORTHANC_PLUGIN_PEERS  0
+#  define HAS_ORTHANC_PLUGIN_JOB    0
+#endif
+
+
+
+namespace OrthancPlugins
+{
+  typedef void (*RestCallback) (OrthancPluginRestOutput* output,
+                                const char* url,
+                                const OrthancPluginHttpRequest* request);
+
+  class OrthancImage;
+
+
+  class MemoryBuffer : public boost::noncopyable
+  {
+  private:
+    OrthancPluginContext*      context_;
+    OrthancPluginMemoryBuffer  buffer_;
+
+    void Check(OrthancPluginErrorCode code);
+
+    bool CheckHttp(OrthancPluginErrorCode code);
+
+  public:
+    MemoryBuffer(OrthancPluginContext* context);
+
+    ~MemoryBuffer()
+    {
+      Clear();
+    }
+
+    OrthancPluginMemoryBuffer* operator*()
+    {
+      return &buffer_;
+    }
+
+    // This transfers ownership from "other" to "this"
+    void Assign(OrthancPluginMemoryBuffer& other);
+
+    OrthancPluginMemoryBuffer Release();
+
+    const char* GetData() const
+    {
+      if (buffer_.size > 0)
+      {
+        return reinterpret_cast<const char*>(buffer_.data);
+      }
+      else
+      {
+        return NULL;
+      }
+    }
+
+    size_t GetSize() const
+    {
+      return buffer_.size;
+    }
+
+    void Clear();
+
+    void ToString(std::string& target) const;
+
+    void ToJson(Json::Value& target) const;
+
+    bool RestApiGet(const std::string& uri,
+                    bool applyPlugins);
+
+    bool RestApiPost(const std::string& uri,
+                     const char* body,
+                     size_t bodySize,
+                     bool applyPlugins);
+
+    bool RestApiPut(const std::string& uri,
+                    const char* body,
+                    size_t bodySize,
+                    bool applyPlugins);
+
+    bool RestApiPost(const std::string& uri,
+                     const Json::Value& body,
+                     bool applyPlugins);
+
+    bool RestApiPut(const std::string& uri,
+                    const Json::Value& body,
+                    bool applyPlugins);
+
+    bool RestApiPost(const std::string& uri,
+                     const std::string& body,
+                     bool applyPlugins)
+    {
+      return RestApiPost(uri, body.empty() ? NULL : body.c_str(), body.size(), applyPlugins);
+    }
+
+    bool RestApiPut(const std::string& uri,
+                    const std::string& body,
+                    bool applyPlugins)
+    {
+      return RestApiPut(uri, body.empty() ? NULL : body.c_str(), body.size(), applyPlugins);
+    }
+
+    void CreateDicom(const Json::Value& tags,
+                     OrthancPluginCreateDicomFlags flags);
+
+    void CreateDicom(const Json::Value& tags,
+                     const OrthancImage& pixelData,
+                     OrthancPluginCreateDicomFlags flags);
+
+    void ReadFile(const std::string& path);
+
+    void GetDicomQuery(const OrthancPluginWorklistQuery* query);
+
+    void DicomToJson(Json::Value& target,
+                     OrthancPluginDicomToJsonFormat format,
+                     OrthancPluginDicomToJsonFlags flags,
+                     uint32_t maxStringLength);
+
+    bool HttpGet(const std::string& url,
+                 const std::string& username,
+                 const std::string& password);
+ 
+    bool HttpPost(const std::string& url,
+                  const std::string& body,
+                  const std::string& username,
+                  const std::string& password);
+ 
+    bool HttpPut(const std::string& url,
+                 const std::string& body,
+                 const std::string& username,
+                 const std::string& password);
+
+    void GetDicomInstance(const std::string& instanceId);
+  };
+
+
+  class OrthancString : public boost::noncopyable
+  {
+  private:
+    OrthancPluginContext*  context_;
+    char*                  str_;
+
+    void Clear();
+
+  public:
+    OrthancString(OrthancPluginContext* context) :
+      context_(context),
+      str_(NULL)
+    {
+    }
+
+    ~OrthancString()
+    {
+      Clear();
+    }
+
+    // This transfers ownership, warning: The string must have been
+    // allocated by the Orthanc core
+    void Assign(char* str);
+
+    const char* GetContent() const
+    {
+      return str_;
+    }
+
+    void ToString(std::string& target) const;
+
+    void ToJson(Json::Value& target) const;
+  };
+
+
+  class OrthancConfiguration : public boost::noncopyable
+  {
+  private:
+    OrthancPluginContext*  context_;
+    Json::Value            configuration_;  // Necessarily a Json::objectValue
+    std::string            path_;
+
+    std::string GetPath(const std::string& key) const;
+
+  public:
+    OrthancConfiguration() : context_(NULL)
+    {
+    }
+
+    OrthancConfiguration(OrthancPluginContext* context);
+
+    OrthancPluginContext* GetContext() const;
+
+    const Json::Value& GetJson() const
+    {
+      return configuration_;
+    }
+
+    bool IsSection(const std::string& key) const;
+
+    void GetSection(OrthancConfiguration& target,
+                    const std::string& key) const;
+
+    bool LookupStringValue(std::string& target,
+                           const std::string& key) const;
+    
+    bool LookupIntegerValue(int& target,
+                            const std::string& key) const;
+
+    bool LookupUnsignedIntegerValue(unsigned int& target,
+                                    const std::string& key) const;
+
+    bool LookupBooleanValue(bool& target,
+                            const std::string& key) const;
+
+    bool LookupFloatValue(float& target,
+                          const std::string& key) const;
+
+    bool LookupListOfStrings(std::list<std::string>& target,
+                             const std::string& key,
+                             bool allowSingleString) const;
+
+    bool LookupSetOfStrings(std::set<std::string>& target,
+                            const std::string& key,
+                            bool allowSingleString) const;
+
+    std::string GetStringValue(const std::string& key,
+                               const std::string& defaultValue) const;
+
+    int GetIntegerValue(const std::string& key,
+                        int defaultValue) const;
+
+    unsigned int GetUnsignedIntegerValue(const std::string& key,
+                                         unsigned int defaultValue) const;
+
+    bool GetBooleanValue(const std::string& key,
+                         bool defaultValue) const;
+
+    float GetFloatValue(const std::string& key,
+                        float defaultValue) const;
+
+    void GetDictionary(std::map<std::string, std::string>& target,
+                       const std::string& key) const;
+  };
+
+  class OrthancImage : public boost::noncopyable
+  {
+  private:
+    OrthancPluginContext*  context_;
+    OrthancPluginImage*    image_;
+
+    void Clear();
+
+    void CheckImageAvailable();
+
+  public:
+    OrthancImage(OrthancPluginContext*  context);
+
+    OrthancImage(OrthancPluginContext*  context,
+                 OrthancPluginImage*    image);
+
+    OrthancImage(OrthancPluginContext*     context,
+                 OrthancPluginPixelFormat  format,
+                 uint32_t                  width,
+                 uint32_t                  height);
+
+    OrthancImage(OrthancPluginContext*     context,
+                 OrthancPluginPixelFormat  format,
+                 uint32_t                  width,
+                 uint32_t                  height,
+                 uint32_t                  pitch,
+                 void*                     buffer
+                 );
+
+    ~OrthancImage()
+    {
+      Clear();
+    }
+
+    void UncompressPngImage(const void* data,
+                            size_t size);
+
+    void UncompressJpegImage(const void* data,
+                             size_t size);
+
+    void DecodeDicomImage(const void* data,
+                          size_t size,
+                          unsigned int frame);
+
+    OrthancPluginPixelFormat GetPixelFormat();
+
+    unsigned int GetWidth();
+
+    unsigned int GetHeight();
+
+    unsigned int GetPitch();
+    
+    const void* GetBuffer();
+
+    const OrthancPluginImage* GetObject() const
+    {
+      return image_;
+    }
+
+    void CompressPngImage(MemoryBuffer& target);
+
+    void CompressJpegImage(MemoryBuffer& target,
+                           uint8_t quality);
+
+    void AnswerPngImage(OrthancPluginRestOutput* output);
+
+    void AnswerJpegImage(OrthancPluginRestOutput* output,
+                         uint8_t quality);
+  };
+
+
+#if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1
+  class FindMatcher : public boost::noncopyable
+  {
+  private:
+    OrthancPluginContext*              context_;
+    OrthancPluginFindMatcher*          matcher_;
+    const OrthancPluginWorklistQuery*  worklist_;
+
+    void SetupDicom(OrthancPluginContext*  context,
+                    const void*            query,
+                    uint32_t               size);
+
+  public:
+    FindMatcher(OrthancPluginContext*              context,
+                const OrthancPluginWorklistQuery*  worklist);
+
+    FindMatcher(OrthancPluginContext*  context,
+                const void*            query,
+                uint32_t               size)
+    {
+      SetupDicom(context, query, size);
+    }
+
+    FindMatcher(OrthancPluginContext*  context,
+                const MemoryBuffer&    dicom)
+    {
+      SetupDicom(context, dicom.GetData(), dicom.GetSize());
+    }
+
+    ~FindMatcher();
+
+    bool IsMatch(const void*  dicom,
+                 uint32_t     size) const;
+
+    bool IsMatch(const MemoryBuffer& dicom) const
+    {
+      return IsMatch(dicom.GetData(), dicom.GetSize());
+    }
+  };
+#endif
+
+
+  bool RestApiGet(Json::Value& result,
+                  OrthancPluginContext* context,
+                  const std::string& uri,
+                  bool applyPlugins);
+
+  bool RestApiPost(Json::Value& result,
+                   OrthancPluginContext* context,
+                   const std::string& uri,
+                   const char* body,
+                   size_t bodySize,
+                   bool applyPlugins);
+
+  bool RestApiPost(Json::Value& result,
+                   OrthancPluginContext* context,
+                   const std::string& uri,
+                   const Json::Value& body,
+                   bool applyPlugins);
+
+  inline bool RestApiPost(Json::Value& result,
+                          OrthancPluginContext* context,
+                          const std::string& uri,
+                          const std::string& body,
+                          bool applyPlugins)
+  {
+    return RestApiPost(result, context, uri, body.empty() ? NULL : body.c_str(), 
+                       body.size(), applyPlugins);
+  }
+
+  bool RestApiPut(Json::Value& result,
+                  OrthancPluginContext* context,
+                  const std::string& uri,
+                  const char* body,
+                  size_t bodySize,
+                  bool applyPlugins);
+
+  bool RestApiPut(Json::Value& result,
+                  OrthancPluginContext* context,
+                  const std::string& uri,
+                  const Json::Value& body,
+                  bool applyPlugins);
+
+  inline bool RestApiPut(Json::Value& result,
+                         OrthancPluginContext* context,
+                         const std::string& uri,
+                         const std::string& body,
+                         bool applyPlugins)
+  {
+    return RestApiPut(result, context, uri, body.empty() ? NULL : body.c_str(), 
+                      body.size(), applyPlugins);
+  }
+
+  bool RestApiDelete(OrthancPluginContext* context,
+                     const std::string& uri,
+                     bool applyPlugins);
+
+  bool HttpDelete(OrthancPluginContext* context,
+                  const std::string& url,
+                  const std::string& username,
+                  const std::string& password);
+
+  inline void LogError(OrthancPluginContext* context,
+                       const std::string& message)
+  {
+    if (context != NULL)
+    {
+      OrthancPluginLogError(context, message.c_str());
+    }
+  }
+
+  inline void LogWarning(OrthancPluginContext* context,
+                         const std::string& message)
+  {
+    if (context != NULL)
+    {
+      OrthancPluginLogWarning(context, message.c_str());
+    }
+  }
+
+  inline void LogInfo(OrthancPluginContext* context,
+                      const std::string& message)
+  {
+    if (context != NULL)
+    {
+      OrthancPluginLogInfo(context, message.c_str());
+    }
+  }
+
+  void ReportMinimalOrthancVersion(OrthancPluginContext* context,
+                                   unsigned int major,
+                                   unsigned int minor,
+                                   unsigned int revision);
+  
+  bool CheckMinimalOrthancVersion(OrthancPluginContext* context,
+                                  unsigned int major,
+                                  unsigned int minor,
+                                  unsigned int revision);
+
+
+  namespace Internals
+  {
+    template <RestCallback Callback>
+    OrthancPluginErrorCode Protect(OrthancPluginRestOutput* output,
+                                   const char* url,
+                                   const OrthancPluginHttpRequest* request)
+    {
+      try
+      {
+        Callback(output, url, request);
+        return OrthancPluginErrorCode_Success;
+      }
+      catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+      {
+        return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        return OrthancPluginErrorCode_BadFileFormat;
+      }
+      catch (...)
+      {
+        return OrthancPluginErrorCode_Plugin;
+      }
+    }
+  }
+
+  
+  template <RestCallback Callback>
+  void RegisterRestCallback(OrthancPluginContext* context,
+                            const std::string& uri,
+                            bool isThreadSafe)
+  {
+    if (isThreadSafe)
+    {
+      OrthancPluginRegisterRestCallbackNoLock(context, uri.c_str(), Internals::Protect<Callback>);
+    }
+    else
+    {
+      OrthancPluginRegisterRestCallback(context, uri.c_str(), Internals::Protect<Callback>);
+    }
+  }
+
+
+#if HAS_ORTHANC_PLUGIN_PEERS == 1
+  class OrthancPeers : public boost::noncopyable
+  {
+  private:
+    typedef std::map<std::string, uint32_t>   Index;
+
+    OrthancPluginContext *context_;
+    OrthancPluginPeers   *peers_;
+    Index                 index_;
+    uint32_t              timeout_;
+
+    size_t GetPeerIndex(const std::string& name) const;
+
+  public:
+    OrthancPeers(OrthancPluginContext* context);
+
+    ~OrthancPeers();
+
+    uint32_t GetTimeout() const
+    {
+      return timeout_;
+    }
+
+    void SetTimeout(uint32_t timeout)
+    {
+      timeout_ = timeout;
+    }
+
+    bool LookupName(size_t& target,
+                    const std::string& name) const;
+
+    std::string GetPeerName(size_t index) const;
+
+    std::string GetPeerUrl(size_t index) const;
+
+    std::string GetPeerUrl(const std::string& name) const;
+
+    size_t GetPeersCount() const
+    {
+      return index_.size();
+    }
+
+    bool LookupUserProperty(std::string& value,
+                            size_t index,
+                            const std::string& key) const;
+
+    bool LookupUserProperty(std::string& value,
+                            const std::string& peer,
+                            const std::string& key) const;
+
+    bool DoGet(MemoryBuffer& target,
+               size_t index,
+               const std::string& uri) const;
+
+    bool DoGet(MemoryBuffer& target,
+               const std::string& name,
+               const std::string& uri) const;
+
+    bool DoGet(Json::Value& target,
+               size_t index,
+               const std::string& uri) const;
+      
+    bool DoGet(Json::Value& target,
+               const std::string& name,
+               const std::string& uri) const;
+      
+    bool DoPost(MemoryBuffer& target,
+                size_t index,
+                const std::string& uri,
+                const std::string& body) const;
+
+    bool DoPost(MemoryBuffer& target,
+                const std::string& name,
+                const std::string& uri,
+                const std::string& body) const;
+
+    bool DoPost(Json::Value& target,
+                size_t index,
+                const std::string& uri,
+                const std::string& body) const;
+      
+    bool DoPost(Json::Value& target,
+                const std::string& name,
+                const std::string& uri,
+                const std::string& body) const;
+
+    bool DoPut(size_t index,
+               const std::string& uri,
+               const std::string& body) const;
+
+    bool DoPut(const std::string& name,
+               const std::string& uri,
+               const std::string& body) const;
+
+    bool DoDelete(size_t index,
+                  const std::string& uri) const;
+
+    bool DoDelete(const std::string& name,
+                  const std::string& uri) const;
+  };
+#endif
+
+
+
+#if HAS_ORTHANC_PLUGIN_JOB == 1
+  class OrthancJob : public boost::noncopyable
+  {
+  private:
+    std::string   jobType_;
+    std::string   content_;
+    bool          hasSerialized_;
+    std::string   serialized_;
+    float         progress_;
+
+    static void CallbackFinalize(void* job);
+
+    static float CallbackGetProgress(void* job);
+
+    static const char* CallbackGetContent(void* job);
+
+    static const char* CallbackGetSerialized(void* job);
+
+    static OrthancPluginJobStepStatus CallbackStep(void* job);
+
+    static OrthancPluginErrorCode CallbackStop(void* job,
+                                               OrthancPluginJobStopReason reason);
+
+    static OrthancPluginErrorCode CallbackReset(void* job);
+
+  protected:
+    void ClearContent();
+
+    void UpdateContent(const Json::Value& content);
+
+    void ClearSerialized();
+
+    void UpdateSerialized(const Json::Value& serialized);
+
+    void UpdateProgress(float progress);
+    
+  public:
+    OrthancJob(const std::string& jobType);
+    
+    virtual ~OrthancJob()
+    {
+    }
+
+    virtual OrthancPluginJobStepStatus Step() = 0;
+
+    virtual void Stop(OrthancPluginJobStopReason reason) = 0;
+    
+    virtual void Reset() = 0;
+
+    static OrthancPluginJob* Create(OrthancPluginContext* context,
+                                    OrthancJob* job /* takes ownership */);
+
+    static std::string Submit(OrthancPluginContext* context,
+                              OrthancJob* job /* takes ownership */,
+                              int priority);
+  };
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/OrthancPluginException.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,101 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(HAS_ORTHANC_EXCEPTION)
+#  error The macro HAS_ORTHANC_EXCEPTION must be defined
+#endif
+
+
+#if HAS_ORTHANC_EXCEPTION == 1
+#  include "../../../Core/OrthancException.h"
+#  define ORTHANC_PLUGINS_ERROR_ENUMERATION     ::Orthanc::ErrorCode
+#  define ORTHANC_PLUGINS_EXCEPTION_CLASS       ::Orthanc::OrthancException
+#  define ORTHANC_PLUGINS_GET_ERROR_CODE(code)  ::Orthanc::ErrorCode_ ## code
+#else
+#  include <orthanc/OrthancCPlugin.h>
+#  define ORTHANC_PLUGINS_ERROR_ENUMERATION     ::OrthancPluginErrorCode
+#  define ORTHANC_PLUGINS_EXCEPTION_CLASS       ::OrthancPlugins::PluginException
+#  define ORTHANC_PLUGINS_GET_ERROR_CODE(code)  ::OrthancPluginErrorCode_ ## code
+#endif
+
+
+#define ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code)                   \
+  throw ORTHANC_PLUGINS_EXCEPTION_CLASS(static_cast<ORTHANC_PLUGINS_ERROR_ENUMERATION>(code));
+
+
+#define ORTHANC_PLUGINS_THROW_EXCEPTION(code)                           \
+  throw ORTHANC_PLUGINS_EXCEPTION_CLASS(ORTHANC_PLUGINS_GET_ERROR_CODE(code));
+                                                  
+
+#define ORTHANC_PLUGINS_CHECK_ERROR(code)                           \
+  if (code != ORTHANC_PLUGINS_GET_ERROR_CODE(Success))              \
+  {                                                                 \
+    ORTHANC_PLUGINS_THROW_EXCEPTION(code);                          \
+  }
+
+
+namespace OrthancPlugins
+{
+#if HAS_ORTHANC_EXCEPTION == 0
+  class PluginException
+  {
+  private:
+    OrthancPluginErrorCode  code_;
+
+  public:
+    explicit PluginException(OrthancPluginErrorCode code) : code_(code)
+    {
+    }
+
+    OrthancPluginErrorCode GetErrorCode() const
+    {
+      return code_;
+    }
+
+    const char* What(OrthancPluginContext* context) const
+    {
+      const char* description = OrthancPluginGetErrorDescription(context, code_);
+      if (description)
+      {
+        return description;
+      }
+      else
+      {
+        return "No description available";
+      }
+    }
+  };
+#endif
+}
--- a/Plugins/Samples/Common/OrthancPlugins.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Samples/Common/OrthancPlugins.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -17,7 +17,7 @@
 if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
   # Linking with "pthread" is necessary, otherwise the software crashes
   # http://sourceware.org/bugzilla/show_bug.cgi?id=10652#c17
-  link_libraries(dl rt)
+  link_libraries(dl rt pthread)
 endif()
 
 
@@ -27,3 +27,6 @@
 if (MSVC)
   include_directories(${SAMPLES_ROOT}/../../Resources/ThirdParty/VisualStudio/)
 endif()
+
+
+add_definitions(-DHAS_ORTHANC_EXCEPTION=0)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/SimplifiedOrthancDataset.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,157 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "SimplifiedOrthancDataset.h"
+
+#include "OrthancPluginException.h"
+
+namespace OrthancPlugins
+{
+  const Json::Value* SimplifiedOrthancDataset::LookupPath(const DicomPath& path) const
+  {
+    const Json::Value* content = &root_;
+                                  
+    for (unsigned int depth = 0; depth < path.GetPrefixLength(); depth++)
+    {
+      const char* name = path.GetPrefixTag(depth).GetName();
+      if (content->type() != Json::objectValue)
+      {
+        ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+      }
+
+      if (!content->isMember(name))
+      {
+        return NULL;
+      }
+
+      const Json::Value& sequence = (*content) [name];
+      if (sequence.type() != Json::arrayValue)
+      {
+        ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+      }
+
+      size_t index = path.GetPrefixIndex(depth);
+      if (index >= sequence.size())
+      {
+        return NULL;
+      }
+      else
+      {
+        content = &sequence[static_cast<Json::Value::ArrayIndex>(index)];
+      }
+    }
+
+    const char* name = path.GetFinalTag().GetName();
+
+    if (content->type() != Json::objectValue)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+    if (!content->isMember(name))
+    {
+      return NULL;
+    }
+    else
+    {
+      return &((*content) [name]);
+    }
+  }
+
+
+  void SimplifiedOrthancDataset::CheckRoot() const
+  {
+    if (root_.type() != Json::objectValue)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+  }
+
+
+  SimplifiedOrthancDataset::SimplifiedOrthancDataset(IOrthancConnection& orthanc,
+                                                     const std::string& uri)
+  {
+    IOrthancConnection::RestApiGet(root_, orthanc, uri);
+    CheckRoot();
+  }
+
+
+  SimplifiedOrthancDataset::SimplifiedOrthancDataset(const std::string& content)
+  {
+    IOrthancConnection::ParseJson(root_, content);
+    CheckRoot();
+  }
+
+
+  bool SimplifiedOrthancDataset::GetStringValue(std::string& result,
+                                                const DicomPath& path) const
+  {
+    const Json::Value* value = LookupPath(path);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else if (value->type() != Json::stringValue)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+    else
+    {
+      result = value->asString();
+      return true;
+    }
+  }
+
+
+  bool SimplifiedOrthancDataset::GetSequenceSize(size_t& size,
+                                                 const DicomPath& path) const
+  {
+    const Json::Value* sequence = LookupPath(path);
+
+    if (sequence == NULL)
+    {
+      // Inexistent path
+      return false;
+    }
+    else if (sequence->type() != Json::arrayValue)
+    {
+      // Not a sequence
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+    else
+    {
+      size = sequence->size();
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Common/SimplifiedOrthancDataset.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IOrthancConnection.h"
+#include "IDicomDataset.h"
+
+namespace OrthancPlugins
+{
+  class SimplifiedOrthancDataset : public IDicomDataset
+  {
+  private:
+    Json::Value   root_;
+
+    const Json::Value* LookupPath(const DicomPath& path) const;
+
+    void CheckRoot() const;
+
+  public:
+    SimplifiedOrthancDataset(IOrthancConnection& orthanc,
+                             const std::string& uri);
+
+    SimplifiedOrthancDataset(const std::string& content);
+
+    virtual bool GetStringValue(std::string& result,
+                                const DicomPath& path) const;
+
+    virtual bool GetSequenceSize(size_t& size,
+                                 const DicomPath& path) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/CustomImageDecoder/CMakeLists.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,8 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(CustomImageDecoder)
+
+set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
+include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake)
+
+add_library(PluginTest SHARED Plugin.cpp)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/CustomImageDecoder/Plugin.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,81 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include <orthanc/OrthancCPlugin.h>
+
+static OrthancPluginContext* context_ = NULL;
+
+
+static OrthancPluginErrorCode DecodeImageCallback(OrthancPluginImage** target,
+                                                  const void* dicom,
+                                                  const uint32_t size,
+                                                  uint32_t frameIndex)
+{
+  *target = OrthancPluginCreateImage(context_, OrthancPluginPixelFormat_RGB24, 512, 512);
+
+  memset(OrthancPluginGetImageBuffer(context_, *target), 128,
+         OrthancPluginGetImageHeight(context_, *target) * OrthancPluginGetImagePitch(context_, *target));
+
+  return OrthancPluginDrawText(context_, *target, 0, "Hello world", 100, 50, 255, 0, 0);
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    context_ = c;
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(c) == 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;
+    }
+
+    OrthancPluginRegisterDecodeImageCallback(context_, DecodeImageCallback);
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "custom-image-decoder";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return "0.0";
+  }
+}
--- a/Plugins/Samples/DatabasePlugin/CMakeLists.txt	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-cmake_minimum_required(VERSION 2.8)
-
-project(SampleDatabasePlugin)
-
-# Parameters of the build
-SET(SAMPLE_DATABASE_VERSION "0.0" CACHE STRING "Version of the plugin")
-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")
-SET(STANDALONE_BUILD ON)
-
-# 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_SQLITE ON CACHE BOOL "Use the system version of SQLite")
-
-set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
-include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake)
-
-include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/SQLiteConfiguration.cmake)
-
-EmbedResources(
-  --system-exception  # Use "std::runtime_error" instead of "OrthancException" for embedded resources
-  PREPARE_DATABASE  ${ORTHANC_ROOT}/OrthancServer/PrepareDatabase.sql
-  )
-
-message("Setting the version of the plugin to ${SAMPLE_DATABASE_VERSION}")
-
-add_definitions(
-  -DORTHANC_SQLITE_STANDALONE=1
-  -DORTHANC_ENABLE_LOGGING=0
-  -DORTHANC_ENABLE_BASE64=0
-  -DORTHANC_ENABLE_MD5=0
-  -DORTHANC_ENABLE_DCMTK=0
-  -DORTHANC_PLUGINS_ENABLED=1
-  -DSAMPLE_DATABASE_VERSION="${SAMPLE_DATABASE_VERSION}"
-  )
-
-add_library(SampleDatabase SHARED 
-  ${BOOST_SOURCES}
-  ${JSONCPP_SOURCES}
-  ${SQLITE_SOURCES}
-  ${AUTOGENERATED_SOURCES}
-
-  ${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/SQLite/Connection.cpp
-  ${ORTHANC_ROOT}/Core/SQLite/FunctionContext.cpp
-  ${ORTHANC_ROOT}/Core/SQLite/Statement.cpp
-  ${ORTHANC_ROOT}/Core/SQLite/StatementId.cpp
-  ${ORTHANC_ROOT}/Core/SQLite/StatementReference.cpp
-  ${ORTHANC_ROOT}/Core/SQLite/Transaction.cpp
-  ${ORTHANC_ROOT}/Core/Toolbox.cpp
-  ${ORTHANC_ROOT}/OrthancServer/DatabaseWrapperBase.cpp
-  ${ORTHANC_ROOT}/Plugins/Engine/PluginsEnumerations.cpp
-
-  Database.cpp
-  Plugin.cpp
-  )
-
-set_target_properties(SampleDatabase PROPERTIES 
-  VERSION ${SAMPLE_DATABASE_VERSION} 
-  SOVERSION ${SAMPLE_DATABASE_VERSION})
-
-install(
-  TARGETS SampleDatabase
-  RUNTIME DESTINATION lib    # Destination for Windows
-  LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
-  )
--- a/Plugins/Samples/DatabasePlugin/Database.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,562 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "Database.h"
-
-#include "../../../Core/DicomFormat/DicomArray.h"
-
-#include <EmbeddedResources.h>
-#include <boost/lexical_cast.hpp>
-
-
-namespace Internals
-{
-  class SignalFileDeleted : public Orthanc::SQLite::IScalarFunction
-  {
-  private:
-    OrthancPlugins::DatabaseBackendOutput&  output_;
-
-  public:
-    SignalFileDeleted(OrthancPlugins::DatabaseBackendOutput&  output) : output_(output)
-    {
-    }
-
-    virtual const char* GetName() const
-    {
-      return "SignalFileDeleted";
-    }
-
-    virtual unsigned int GetCardinality() const
-    {
-      return 7;
-    }
-
-    virtual void Compute(Orthanc::SQLite::FunctionContext& context)
-    {
-      std::string uncompressedMD5, compressedMD5;
-
-      if (!context.IsNullValue(5))
-      {
-        uncompressedMD5 = context.GetStringValue(5);
-      }
-
-      if (!context.IsNullValue(6))
-      {
-        compressedMD5 = context.GetStringValue(6);
-      }
-      
-      output_.SignalDeletedAttachment(context.GetStringValue(0),
-                                      context.GetIntValue(1),
-                                      context.GetInt64Value(2),
-                                      uncompressedMD5,
-                                      context.GetIntValue(3),
-                                      context.GetInt64Value(4),
-                                      compressedMD5);
-    }
-  };
-
-
-  class SignalResourceDeleted : public Orthanc::SQLite::IScalarFunction
-  {
-  private:
-    OrthancPlugins::DatabaseBackendOutput&  output_;
-
-  public:
-    SignalResourceDeleted(OrthancPlugins::DatabaseBackendOutput&  output) : output_(output)
-    {
-    }
-
-    virtual const char* GetName() const
-    {
-      return "SignalResourceDeleted";
-    }
-
-    virtual unsigned int GetCardinality() const
-    {
-      return 2;
-    }
-
-    virtual void Compute(Orthanc::SQLite::FunctionContext& context)
-    {
-      output_.SignalDeletedResource(context.GetStringValue(0),
-                                    Orthanc::Plugins::Convert(static_cast<Orthanc::ResourceType>(context.GetIntValue(1))));
-    }
-  };
-}
-
-
-class Database::SignalRemainingAncestor : public Orthanc::SQLite::IScalarFunction
-{
-private:
-  bool hasRemainingAncestor_;
-  std::string remainingPublicId_;
-  OrthancPluginResourceType remainingType_;
-
-public:
-  SignalRemainingAncestor() : 
-    hasRemainingAncestor_(false)
-  {
-  }
-
-  void Reset()
-  {
-    hasRemainingAncestor_ = false;
-  }
-
-  virtual const char* GetName() const
-  {
-    return "SignalRemainingAncestor";
-  }
-
-  virtual unsigned int GetCardinality() const
-  {
-    return 2;
-  }
-
-  virtual void Compute(Orthanc::SQLite::FunctionContext& context)
-  {
-    if (!hasRemainingAncestor_ ||
-        remainingType_ >= context.GetIntValue(1))
-    {
-      hasRemainingAncestor_ = true;
-      remainingPublicId_ = context.GetStringValue(0);
-      remainingType_ = Orthanc::Plugins::Convert(static_cast<Orthanc::ResourceType>(context.GetIntValue(1)));
-    }
-  }
-
-  bool HasRemainingAncestor() const
-  {
-    return hasRemainingAncestor_;
-  }
-
-  const std::string& GetRemainingAncestorId() const
-  {
-    assert(hasRemainingAncestor_);
-    return remainingPublicId_;
-  }
-
-  OrthancPluginResourceType GetRemainingAncestorType() const
-  {
-    assert(hasRemainingAncestor_);
-    return remainingType_;
-  }
-};
-
-
-
-Database::Database(const std::string& path) : 
-  path_(path),
-  base_(db_)
-{
-}
-
-
-void Database::Open()
-{
-  db_.Open(path_);
-
-  db_.Execute("PRAGMA ENCODING=\"UTF-8\";");
-
-  // http://www.sqlite.org/pragma.html
-  db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;");
-  db_.Execute("PRAGMA JOURNAL_MODE=WAL;");
-  db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;");
-  db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;");
-  //db_.Execute("PRAGMA TEMP_STORE=memory");
-
-  if (!db_.DoesTableExist("GlobalProperties"))
-  {
-    std::string query;
-    Orthanc::EmbeddedResources::GetFileResource(query, Orthanc::EmbeddedResources::PREPARE_DATABASE);
-    db_.Execute(query);
-  }
-
-  signalRemainingAncestor_ = new SignalRemainingAncestor;
-  db_.Register(signalRemainingAncestor_);
-  db_.Register(new Internals::SignalFileDeleted(GetOutput()));
-  db_.Register(new Internals::SignalResourceDeleted(GetOutput()));
-}
-
-
-void Database::Close()
-{
-  db_.Close();
-}
-
-
-void Database::AddAttachment(int64_t id,
-                             const OrthancPluginAttachment& attachment)
-{
-  Orthanc::FileInfo info(attachment.uuid,
-                         static_cast<Orthanc::FileContentType>(attachment.contentType),
-                         attachment.uncompressedSize,
-                         attachment.uncompressedHash,
-                         static_cast<Orthanc::CompressionType>(attachment.compressionType),
-                         attachment.compressedSize,
-                         attachment.compressedHash);
-  base_.AddAttachment(id, info);
-}
-
-
-void Database::DeleteResource(int64_t id)
-{
-  signalRemainingAncestor_->Reset();
-
-  Orthanc::SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?");
-  s.BindInt64(0, id);
-  s.Run();
-
-  if (signalRemainingAncestor_->HasRemainingAncestor())
-  {
-    GetOutput().SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorId(),
-                                        signalRemainingAncestor_->GetRemainingAncestorType());
-  }
-}
-
-
-static void Answer(OrthancPlugins::DatabaseBackendOutput& output,
-                   const Orthanc::ServerIndexChange& change)
-{
-  output.AnswerChange(change.GetSeq(), 
-                      change.GetChangeType(),
-                      Orthanc::Plugins::Convert(change.GetResourceType()),
-                      change.GetPublicId(),
-                      change.GetDate());
-}
-
-
-static void Answer(OrthancPlugins::DatabaseBackendOutput& output,
-                   const Orthanc::ExportedResource& resource)
-{
-  output.AnswerExportedResource(resource.GetSeq(),
-                                Orthanc::Plugins::Convert(resource.GetResourceType()),
-                                resource.GetPublicId(),
-                                resource.GetModality(),
-                                resource.GetDate(),
-                                resource.GetPatientId(),
-                                resource.GetStudyInstanceUid(),
-                                resource.GetSeriesInstanceUid(),
-                                resource.GetSopInstanceUid());
-}
-
-
-void Database::GetChanges(bool& done /*out*/,
-                          int64_t since,
-                          uint32_t maxResults)
-{
-  typedef std::list<Orthanc::ServerIndexChange> Changes;
-
-  Changes changes;
-  base_.GetChanges(changes, done, since, maxResults);
-
-  for (Changes::const_iterator it = changes.begin(); it != changes.end(); ++it)
-  {
-    Answer(GetOutput(), *it);
-  }
-}
-
-
-void Database::GetExportedResources(bool& done /*out*/,
-                                    int64_t since,
-                                    uint32_t maxResults)
-{
-  typedef std::list<Orthanc::ExportedResource> Resources;
-
-  Resources resources;
-  base_.GetExportedResources(resources, done, since, maxResults);
-
-  for (Resources::const_iterator it = resources.begin(); it != resources.end(); ++it)
-  {
-    Answer(GetOutput(), *it);
-  }
-}
-
-
-void Database::GetLastChange()
-{
-  std::list<Orthanc::ServerIndexChange> change;
-  Orthanc::ErrorCode code = base_.GetLastChange(change);
-  
-  if (code != Orthanc::ErrorCode_Success)
-  {
-    throw OrthancPlugins::DatabaseException(static_cast<OrthancPluginErrorCode>(code));
-  }
-
-  if (!change.empty())
-  {
-    Answer(GetOutput(), change.front());
-  }
-}
-
-
-void Database::GetLastExportedResource()
-{
-  std::list<Orthanc::ExportedResource> resource;
-  base_.GetLastExportedResource(resource);
-  
-  if (!resource.empty())
-  {
-    Answer(GetOutput(), resource.front());
-  }
-}
-
-
-void Database::GetMainDicomTags(int64_t id)
-{
-  Orthanc::DicomMap tags;
-  base_.GetMainDicomTags(tags, id);
-
-  Orthanc::DicomArray arr(tags);
-  for (size_t i = 0; i < arr.GetSize(); i++)
-  {
-    GetOutput().AnswerDicomTag(arr.GetElement(i).GetTag().GetGroup(),
-                               arr.GetElement(i).GetTag().GetElement(),
-                               arr.GetElement(i).GetValue().GetContent());
-  }
-}
-
-
-std::string Database::GetPublicId(int64_t resourceId)
-{
-  std::string id;
-  if (base_.GetPublicId(id, resourceId))
-  {
-    return id;
-  }
-  else
-  {
-    throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_UnknownResource);
-  }
-}
-
-
-OrthancPluginResourceType Database::GetResourceType(int64_t resourceId)
-{
-  Orthanc::ResourceType  result;
-  Orthanc::ErrorCode  code = base_.GetResourceType(result, resourceId);
-
-  if (code == Orthanc::ErrorCode_Success)
-  {
-    return Orthanc::Plugins::Convert(result);
-  }
-  else
-  {
-    throw OrthancPlugins::DatabaseException(static_cast<OrthancPluginErrorCode>(code));
-  }
-}
-
-
-
-template <typename I>
-static void ConvertList(std::list<int32_t>& target,
-                        const std::list<I>& source)
-{
-  for (typename std::list<I>::const_iterator 
-         it = source.begin(); it != source.end(); it++)
-  {
-    target.push_back(*it);
-  }
-}
-
-
-void Database::ListAvailableMetadata(std::list<int32_t>& target /*out*/,
-                                     int64_t id)
-{
-  std::list<Orthanc::MetadataType> tmp;
-  base_.ListAvailableMetadata(tmp, id);
-  ConvertList(target, tmp);
-}
-
-
-void Database::ListAvailableAttachments(std::list<int32_t>& target /*out*/,
-                                        int64_t id)
-{
-  std::list<Orthanc::FileContentType> tmp;
-  base_.ListAvailableAttachments(tmp, id);
-  ConvertList(target, tmp);
-}
-
-
-void Database::LogChange(const OrthancPluginChange& change)
-{
-  int64_t id;
-  OrthancPluginResourceType type;
-  if (!LookupResource(id, type, change.publicId) ||
-      type != change.resourceType)
-  {
-    throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_DatabasePlugin);
-  }
-
-  Orthanc::ServerIndexChange tmp(change.seq,
-                                 static_cast<Orthanc::ChangeType>(change.changeType),
-                                 Orthanc::Plugins::Convert(change.resourceType),
-                                 change.publicId,
-                                 change.date);
-
-  base_.LogChange(id, tmp);
-}
-
-
-void Database::LogExportedResource(const OrthancPluginExportedResource& resource) 
-{
-  Orthanc::ExportedResource tmp(resource.seq,
-                                Orthanc::Plugins::Convert(resource.resourceType),
-                                resource.publicId,
-                                resource.modality,
-                                resource.date,
-                                resource.patientId,
-                                resource.studyInstanceUid,
-                                resource.seriesInstanceUid,
-                                resource.sopInstanceUid);
-
-  base_.LogExportedResource(tmp);
-}
-
-    
-bool Database::LookupAttachment(int64_t id,
-                                int32_t contentType)
-{
-  Orthanc::FileInfo attachment;
-  if (base_.LookupAttachment(attachment, id, static_cast<Orthanc::FileContentType>(contentType)))
-  {
-    GetOutput().AnswerAttachment(attachment.GetUuid(),
-                                 attachment.GetContentType(),
-                                 attachment.GetUncompressedSize(),
-                                 attachment.GetUncompressedMD5(),
-                                 attachment.GetCompressionType(),
-                                 attachment.GetCompressedSize(),
-                                 attachment.GetCompressedMD5());
-    return true;
-  }
-  else
-  {
-    return false;
-  }
-}
-
-
-bool Database::LookupParent(int64_t& parentId /*out*/,
-                            int64_t resourceId)
-{
-  bool found;
-  Orthanc::ErrorCode code = base_.LookupParent(found, parentId, resourceId);
-
-  if (code == Orthanc::ErrorCode_Success)
-  {
-    return found;
-  }
-  else
-  {
-    throw OrthancPlugins::DatabaseException(static_cast<OrthancPluginErrorCode>(code));
-  }
-}
-
-
-bool Database::LookupResource(int64_t& id /*out*/,
-                              OrthancPluginResourceType& type /*out*/,
-                              const char* publicId)
-{
-  Orthanc::ResourceType tmp;
-  if (base_.LookupResource(id, tmp, publicId))
-  {
-    type = Orthanc::Plugins::Convert(tmp);
-    return true;
-  }
-  else
-  {
-    return false;
-  }
-}
-
-
-void Database::StartTransaction()
-{
-  transaction_.reset(new Orthanc::SQLite::Transaction(db_));
-  transaction_->Begin();
-}
-
-
-void Database::RollbackTransaction()
-{
-  transaction_->Rollback();
-  transaction_.reset(NULL);
-}
-
-
-void Database::CommitTransaction()
-{
-  transaction_->Commit();
-  transaction_.reset(NULL);
-}
-
-
-uint32_t Database::GetDatabaseVersion()
-{
-  std::string version;
-
-  if (!LookupGlobalProperty(version, Orthanc::GlobalProperty_DatabaseSchemaVersion))
-  {
-    throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_InternalError);
-  }
-
-  try
-  {
-    return boost::lexical_cast<uint32_t>(version);
-  }
-  catch (boost::bad_lexical_cast&)
-  {
-    throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_InternalError);
-  }
-}
-
-
-void Database::UpgradeDatabase(uint32_t  targetVersion,
-                               OrthancPluginStorageArea* storageArea)
-{
-  if (targetVersion == 6)
-  {
-    OrthancPluginErrorCode code = OrthancPluginReconstructMainDicomTags(GetOutput().GetContext(), storageArea, 
-                                                                        OrthancPluginResourceType_Study);
-    if (code == OrthancPluginErrorCode_Success)
-    {
-      code = OrthancPluginReconstructMainDicomTags(GetOutput().GetContext(), storageArea, 
-                                                   OrthancPluginResourceType_Series);
-    }
-
-    if (code != OrthancPluginErrorCode_Success)
-    {
-      throw OrthancPlugins::DatabaseException(code);
-    }
-
-    base_.SetGlobalProperty(Orthanc::GlobalProperty_DatabaseSchemaVersion, "6");
-  }
-}
--- a/Plugins/Samples/DatabasePlugin/Database.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,284 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include <orthanc/OrthancCppDatabasePlugin.h>
-
-#include "../../../Core/SQLite/Connection.h"
-#include "../../../Core/SQLite/Transaction.h"
-#include "../../../OrthancServer/DatabaseWrapperBase.h"
-#include "../../Engine/PluginsEnumerations.h"
-
-#include <memory>
-
-class Database : public OrthancPlugins::IDatabaseBackend
-{
-private:
-  class SignalRemainingAncestor;
-
-  std::string                   path_;
-  Orthanc::SQLite::Connection   db_;
-  Orthanc::DatabaseWrapperBase  base_;
-  SignalRemainingAncestor*      signalRemainingAncestor_;
-
-  std::auto_ptr<Orthanc::SQLite::Transaction>  transaction_;
-
-public:
-  Database(const std::string& path);
-
-  virtual void Open();
-
-  virtual void Close();
-
-  virtual void AddAttachment(int64_t id,
-                             const OrthancPluginAttachment& attachment);
-
-  virtual void AttachChild(int64_t parent,
-                           int64_t child)
-  {
-    base_.AttachChild(parent, child);
-  }
-
-  virtual void ClearChanges()
-  {
-    db_.Execute("DELETE FROM Changes");    
-  }
-
-  virtual void ClearExportedResources()
-  {
-    db_.Execute("DELETE FROM ExportedResources");    
-  }
-
-  virtual int64_t CreateResource(const char* publicId,
-                                 OrthancPluginResourceType type)
-  {
-    return base_.CreateResource(publicId, Orthanc::Plugins::Convert(type));
-  }
-
-  virtual void DeleteAttachment(int64_t id,
-                                int32_t attachment)
-  {
-    base_.DeleteAttachment(id, static_cast<Orthanc::FileContentType>(attachment));
-  }
-
-  virtual void DeleteMetadata(int64_t id,
-                              int32_t metadataType)
-  {
-    base_.DeleteMetadata(id, static_cast<Orthanc::MetadataType>(metadataType));
-  }
-
-  virtual void DeleteResource(int64_t id);
-
-  virtual void GetAllInternalIds(std::list<int64_t>& target,
-                                 OrthancPluginResourceType resourceType)
-  {
-    base_.GetAllInternalIds(target, Orthanc::Plugins::Convert(resourceType));
-  }
-
-  virtual void GetAllPublicIds(std::list<std::string>& target,
-                               OrthancPluginResourceType resourceType)
-  {
-    base_.GetAllPublicIds(target, Orthanc::Plugins::Convert(resourceType));
-  }
-
-  virtual void GetAllPublicIds(std::list<std::string>& target,
-                               OrthancPluginResourceType resourceType,
-                               uint64_t since,
-                               uint64_t limit)
-  {
-    base_.GetAllPublicIds(target, Orthanc::Plugins::Convert(resourceType), since, limit);
-  }
-
-  virtual void GetChanges(bool& done /*out*/,
-                          int64_t since,
-                          uint32_t maxResults);
-
-  virtual void GetChildrenInternalId(std::list<int64_t>& target /*out*/,
-                                     int64_t id)
-  {
-    base_.GetChildrenInternalId(target, id);
-  }
-
-  virtual void GetChildrenPublicId(std::list<std::string>& target /*out*/,
-                                   int64_t id)
-  {
-    base_.GetChildrenPublicId(target, id);
-  }
-
-  virtual void GetExportedResources(bool& done /*out*/,
-                                    int64_t since,
-                                    uint32_t maxResults);
-
-  virtual void GetLastChange();
-
-  virtual void GetLastExportedResource();
-
-  virtual void GetMainDicomTags(int64_t id);
-
-  virtual std::string GetPublicId(int64_t resourceId);
-
-  virtual uint64_t GetResourceCount(OrthancPluginResourceType resourceType)
-  {
-    return base_.GetResourceCount(Orthanc::Plugins::Convert(resourceType));
-  }
-
-  virtual OrthancPluginResourceType GetResourceType(int64_t resourceId);
-
-  virtual uint64_t GetTotalCompressedSize()
-  {
-    return base_.GetTotalCompressedSize();
-  }
-    
-  virtual uint64_t GetTotalUncompressedSize()
-  {
-    return base_.GetTotalUncompressedSize();
-  }
-
-  virtual bool IsExistingResource(int64_t internalId)
-  {
-    return base_.IsExistingResource(internalId);
-  }
-
-  virtual bool IsProtectedPatient(int64_t internalId)
-  {
-    return base_.IsProtectedPatient(internalId);
-  }
-
-  virtual void ListAvailableMetadata(std::list<int32_t>& target /*out*/,
-                                     int64_t id);
-
-  virtual void ListAvailableAttachments(std::list<int32_t>& target /*out*/,
-                                        int64_t id);
-
-  virtual void LogChange(const OrthancPluginChange& change);
-
-  virtual void LogExportedResource(const OrthancPluginExportedResource& resource);
-    
-  virtual bool LookupAttachment(int64_t id,
-                                int32_t contentType);
-
-  virtual bool LookupGlobalProperty(std::string& target /*out*/,
-                                    int32_t property)
-  {
-    return base_.LookupGlobalProperty(target, static_cast<Orthanc::GlobalProperty>(property));
-  }
-
-  virtual void LookupIdentifier(std::list<int64_t>& target /*out*/,
-                                OrthancPluginResourceType level,
-                                uint16_t group,
-                                uint16_t element,
-                                OrthancPluginIdentifierConstraint constraint,
-                                const char* value)
-  {
-    base_.LookupIdentifier(target, Orthanc::Plugins::Convert(level),
-                           Orthanc::DicomTag(group, element), 
-                           Orthanc::Plugins::Convert(constraint), value);
-  }
-
-  virtual bool LookupMetadata(std::string& target /*out*/,
-                              int64_t id,
-                              int32_t metadataType)
-  {
-    return base_.LookupMetadata(target, id, static_cast<Orthanc::MetadataType>(metadataType));
-  }
-
-  virtual bool LookupParent(int64_t& parentId /*out*/,
-                            int64_t resourceId);
-
-  virtual bool LookupResource(int64_t& id /*out*/,
-                              OrthancPluginResourceType& type /*out*/,
-                              const char* publicId);
-
-  virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/)
-  {
-    return base_.SelectPatientToRecycle(internalId);
-  }
-
-  virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/,
-                                      int64_t patientIdToAvoid)
-  {
-    return base_.SelectPatientToRecycle(internalId, patientIdToAvoid);
-  }
-
-
-  virtual void SetGlobalProperty(int32_t property,
-                                 const char* value)
-  {
-    base_.SetGlobalProperty(static_cast<Orthanc::GlobalProperty>(property), value);
-  }
-
-  virtual void SetMainDicomTag(int64_t id,
-                               uint16_t group,
-                               uint16_t element,
-                               const char* value)
-  {
-    base_.SetMainDicomTag(id, Orthanc::DicomTag(group, element), value);
-  }
-
-  virtual void SetIdentifierTag(int64_t id,
-                                uint16_t group,
-                                uint16_t element,
-                                const char* value)
-  {
-    base_.SetIdentifierTag(id, Orthanc::DicomTag(group, element), value);
-  }
-
-  virtual void SetMetadata(int64_t id,
-                           int32_t metadataType,
-                           const char* value)
-  {
-    base_.SetMetadata(id, static_cast<Orthanc::MetadataType>(metadataType), value);
-  }
-
-  virtual void SetProtectedPatient(int64_t internalId, 
-                                   bool isProtected)
-  {
-    base_.SetProtectedPatient(internalId, isProtected);
-  }
-
-  virtual void StartTransaction();
-
-  virtual void RollbackTransaction();
-
-  virtual void CommitTransaction();
-
-  virtual uint32_t GetDatabaseVersion();
-
-  virtual void UpgradeDatabase(uint32_t  targetVersion,
-                               OrthancPluginStorageArea* storageArea);
-
-  virtual void ClearMainDicomTags(int64_t internalId)
-  {
-    base_.ClearMainDicomTags(internalId);
-  }
-};
--- a/Plugins/Samples/DatabasePlugin/Plugin.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "Database.h"
-
-#include <memory>
-#include <iostream>
-#include <boost/algorithm/string/predicate.hpp>
-
-static OrthancPluginContext*  context_ = NULL;
-static std::auto_ptr<OrthancPlugins::IDatabaseBackend>  backend_;
-
-
-extern "C"
-{
-  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
-  {
-    context_ = c;
-    OrthancPluginLogWarning(context_, "Sample plugin is initializing");
-
-    /* Check the version of the Orthanc core */
-    if (OrthancPluginCheckVersion(c) == 0)
-    {
-      char info[256];
-      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
-              c->orthancVersion,
-              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
-              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
-              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
-      OrthancPluginLogError(context_, info);
-      return -1;
-    }
-
-    std::string path = "SampleDatabase.sqlite";
-    uint32_t argCount = OrthancPluginGetCommandLineArgumentsCount(context_);
-    for (uint32_t i = 0; i < argCount; i++)
-    {
-      char* tmp = OrthancPluginGetCommandLineArgument(context_, i);
-      std::string argument(tmp);
-      OrthancPluginFreeString(context_, tmp);
-
-      if (boost::starts_with(argument, "--database="))
-      {
-        path = argument.substr(11);
-      }
-    }
-
-    std::string s = "Using the following SQLite database: " + path;
-    OrthancPluginLogWarning(context_, s.c_str());
-
-    backend_.reset(new Database(path));
-    OrthancPlugins::DatabaseBackendAdapter::Register(context_, *backend_);
-
-    return 0;
-  }
-
-  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
-  {
-    backend_.reset(NULL);
-  }
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
-  {
-    return "sample-database";
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
-  {
-    return "1.0";
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/GdcmDecoder/CMakeLists.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,32 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(GdcmDecoder)
+
+SET(GDCM_DECODER_VERSION "0.0" CACHE STRING "Version of the plugin")
+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")
+SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost")
+
+set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
+include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
+
+find_package(GDCM REQUIRED)
+if (GDCM_FOUND)
+  include(${GDCM_USE_FILE})
+  set(GDCM_LIBRARIES gdcmCommon gdcmMSFF)
+else(GDCM_FOUND)
+  message(FATAL_ERROR "Cannot find GDCM, did you set GDCM_DIR?")
+endif(GDCM_FOUND)
+
+add_definitions(-DGDCM_DECODER_VERSION="${GDCM_DECODER_VERSION}")
+
+add_library(GdcmDecoder SHARED
+  ${BOOST_SOURCES}
+  GdcmDecoderCache.cpp
+  GdcmImageDecoder.cpp
+  OrthancImageWrapper.cpp
+  Plugin.cpp
+  )
+
+target_link_libraries(GdcmDecoder ${GDCM_LIBRARIES})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,99 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "GdcmDecoderCache.h"
+
+#include "OrthancImageWrapper.h"
+
+namespace OrthancPlugins
+{
+  std::string GdcmDecoderCache::ComputeMd5(OrthancPluginContext* context,
+                                           const void* dicom,
+                                           size_t size)
+  {
+    std::string result;
+
+    char* md5 = OrthancPluginComputeMd5(context, dicom, size);
+
+    if (md5 == NULL)
+    {
+      throw std::runtime_error("Cannot compute MD5 hash");
+    }
+
+    bool ok = false;
+    try
+    {
+      result.assign(md5);
+      ok = true;
+    }
+    catch (...)
+    {
+    }
+
+    OrthancPluginFreeString(context, md5);
+
+    if (!ok)
+    {
+      throw std::runtime_error("Not enough memory");
+    }
+    else
+    {    
+      return result;
+    }
+  }
+
+
+  OrthancImageWrapper* GdcmDecoderCache::Decode(OrthancPluginContext* context,
+                                                const void* dicom,
+                                                const uint32_t size,
+                                                uint32_t frameIndex)
+  {
+    std::string md5 = ComputeMd5(context, dicom, size);
+
+    // First check whether the previously decoded image is the same
+    // as this one
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      if (decoder_.get() != NULL &&
+          size_ == size &&
+          md5_ == md5)
+      {
+        // This is the same image: Reuse the previous decoding
+        return new OrthancImageWrapper(context, decoder_->Decode(context, frameIndex));
+      }
+    }
+
+    // This is not the same image
+    std::auto_ptr<GdcmImageDecoder> decoder(new GdcmImageDecoder(dicom, size));
+    std::auto_ptr<OrthancImageWrapper> image(new OrthancImageWrapper(context, decoder->Decode(context, frameIndex)));
+
+    {
+      // Cache the newly created decoder for further use
+      boost::mutex::scoped_lock lock(mutex_);
+      decoder_ = decoder;
+      size_ = size;
+      md5_ = md5;
+    }
+
+    return image.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,54 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "GdcmImageDecoder.h"
+#include "OrthancImageWrapper.h"
+
+#include <boost/thread.hpp>
+
+
+namespace OrthancPlugins
+{
+  class GdcmDecoderCache : public boost::noncopyable
+  {
+  private:
+    boost::mutex   mutex_;
+    std::auto_ptr<OrthancPlugins::GdcmImageDecoder>  decoder_;
+    size_t       size_;
+    std::string  md5_;
+
+    static std::string ComputeMd5(OrthancPluginContext* context,
+                                  const void* dicom,
+                                  size_t size);
+
+  public:
+    GdcmDecoderCache() : size_(0)
+    {
+    }
+
+    OrthancImageWrapper* Decode(OrthancPluginContext* context,
+                                const void* dicom,
+                                const uint32_t size,
+                                uint32_t frameIndex);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,405 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "GdcmImageDecoder.h"
+
+#include "OrthancImageWrapper.h"
+
+#include <gdcmImageReader.h>
+#include <gdcmImageApplyLookupTable.h>
+#include <gdcmImageChangePlanarConfiguration.h>
+#include <gdcmImageChangePhotometricInterpretation.h>
+#include <stdexcept>
+#include <boost/iostreams/stream.hpp>
+#include <boost/iostreams/device/array.hpp>
+
+
+namespace OrthancPlugins
+{
+  struct GdcmImageDecoder::PImpl
+  {
+    const void*           dicom_;
+    size_t                size_;
+
+    gdcm::ImageReader reader_;
+    std::auto_ptr<gdcm::ImageApplyLookupTable> lut_;
+    std::auto_ptr<gdcm::ImageChangePhotometricInterpretation> photometric_;
+    std::auto_ptr<gdcm::ImageChangePlanarConfiguration> interleaved_;
+    std::string decoded_;
+
+    PImpl(const void* dicom,
+          size_t size) :
+      dicom_(dicom),
+      size_(size)
+    {
+    }
+
+
+    const gdcm::DataSet& GetDataSet() const
+    {
+      return reader_.GetFile().GetDataSet();
+    }
+
+
+    const gdcm::Image& GetImage() const
+    {
+      if (interleaved_.get() != NULL)
+      {
+        return interleaved_->GetOutput();
+      }
+
+      if (lut_.get() != NULL)
+      {
+        return lut_->GetOutput();
+      }
+
+      if (photometric_.get() != NULL)
+      {
+        return photometric_->GetOutput();
+      }
+
+      return reader_.GetImage();
+    }
+
+
+    void Decode()
+    {
+      // Change photometric interpretation or apply LUT, if required
+      {
+        const gdcm::Image& image = GetImage();
+        if (image.GetPixelFormat().GetSamplesPerPixel() == 1 &&
+            image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::PALETTE_COLOR)
+        {
+          lut_.reset(new gdcm::ImageApplyLookupTable());
+          lut_->SetInput(image);
+          if (!lut_->Apply())
+          {
+            throw std::runtime_error( "GDCM cannot apply the lookup table");
+          }
+        }
+        else if (image.GetPixelFormat().GetSamplesPerPixel() == 1)
+        {
+          if (image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::MONOCHROME1 &&
+              image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::MONOCHROME2)
+          {
+            photometric_.reset(new gdcm::ImageChangePhotometricInterpretation());
+            photometric_->SetInput(image);
+            photometric_->SetPhotometricInterpretation(gdcm::PhotometricInterpretation::MONOCHROME2);
+            if (!photometric_->Change() ||
+                GetImage().GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::MONOCHROME2)
+            {
+              throw std::runtime_error("GDCM cannot change the photometric interpretation");
+            }
+          }      
+        }
+        else 
+        {
+          if (image.GetPixelFormat().GetSamplesPerPixel() == 3 &&
+              image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::RGB &&
+              image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::YBR_FULL &&
+              (image.GetTransferSyntax() != gdcm::TransferSyntax::JPEG2000Lossless ||
+               image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::YBR_RCT))
+          {
+            photometric_.reset(new gdcm::ImageChangePhotometricInterpretation());
+            photometric_->SetInput(image);
+            photometric_->SetPhotometricInterpretation(gdcm::PhotometricInterpretation::RGB);
+            if (!photometric_->Change() ||
+                GetImage().GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::RGB)
+            {
+              throw std::runtime_error("GDCM cannot change the photometric interpretation");
+            }
+          }
+        }
+      }
+
+      // Possibly convert planar configuration to interleaved
+      {
+        const gdcm::Image& image = GetImage();
+        if (image.GetPlanarConfiguration() != 0 && 
+            image.GetPixelFormat().GetSamplesPerPixel() != 1)
+        {
+          interleaved_.reset(new gdcm::ImageChangePlanarConfiguration());
+          interleaved_->SetInput(image);
+          if (!interleaved_->Change() ||
+              GetImage().GetPlanarConfiguration() != 0)
+          {
+            throw std::runtime_error("GDCM cannot change the planar configuration to interleaved");
+          }
+        }
+      }
+    }
+  };
+
+  GdcmImageDecoder::GdcmImageDecoder(const void* dicom,
+                                     size_t size) :
+    pimpl_(new PImpl(dicom, size))
+  {
+    // Setup a stream to the memory buffer
+    using namespace boost::iostreams;
+    basic_array_source<char> source(reinterpret_cast<const char*>(dicom), size);
+    stream<basic_array_source<char> > stream(source);
+
+    // Parse the DICOM instance using GDCM
+    pimpl_->reader_.SetStream(stream);
+    if (!pimpl_->reader_.Read())
+    {
+      throw std::runtime_error("Bad file format");
+    }
+
+    pimpl_->Decode();
+  }
+
+
+  OrthancPluginPixelFormat GdcmImageDecoder::GetFormat() const
+  {
+    const gdcm::Image& image = pimpl_->GetImage();
+
+    if (image.GetPixelFormat().GetSamplesPerPixel() == 1 &&
+        (image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::MONOCHROME1 ||
+         image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::MONOCHROME2))
+    {
+      switch (image.GetPixelFormat())
+      {
+        case gdcm::PixelFormat::UINT16:
+          return OrthancPluginPixelFormat_Grayscale16;
+
+        case gdcm::PixelFormat::INT16:
+          return OrthancPluginPixelFormat_SignedGrayscale16;
+
+        case gdcm::PixelFormat::UINT8:
+          return OrthancPluginPixelFormat_Grayscale8;
+
+        default:
+          throw std::runtime_error("Unsupported pixel format");
+      }
+    }
+    else if (image.GetPixelFormat().GetSamplesPerPixel() == 3 &&
+             (image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::RGB ||
+              image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::YBR_FULL ||
+              image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::YBR_RCT))
+    {
+      switch (image.GetPixelFormat())
+      {
+        case gdcm::PixelFormat::UINT8:
+          return OrthancPluginPixelFormat_RGB24;
+
+        case gdcm::PixelFormat::UINT16:
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 3, 1)
+          return OrthancPluginPixelFormat_RGB48;
+#else
+          throw std::runtime_error("RGB48 pixel format is only supported if compiled against Orthanc SDK >= 1.3.1");
+#endif
+          
+        default:
+          break;
+      }      
+    }
+
+    throw std::runtime_error("Unsupported pixel format");
+  }
+
+
+  unsigned int GdcmImageDecoder::GetWidth() const
+  {
+    return pimpl_->GetImage().GetColumns();
+  }
+
+
+  unsigned int GdcmImageDecoder::GetHeight() const
+  {
+    return pimpl_->GetImage().GetRows();
+  }
+
+  
+  unsigned int GdcmImageDecoder::GetFramesCount() const
+  {
+    return pimpl_->GetImage().GetDimension(2);
+  }
+
+
+  size_t GdcmImageDecoder::GetBytesPerPixel(OrthancPluginPixelFormat format)
+  {
+    switch (format)
+    {
+      case OrthancPluginPixelFormat_Grayscale8:
+        return 1;
+
+      case OrthancPluginPixelFormat_Grayscale16:
+      case OrthancPluginPixelFormat_SignedGrayscale16:
+        return 2;
+
+      case OrthancPluginPixelFormat_RGB24:
+        return 3;
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 3, 1)
+      case OrthancPluginPixelFormat_RGB48:
+        return 6;
+#endif
+
+      default:
+        throw std::runtime_error("Unsupport pixel format");
+    }
+  }
+
+  static void ConvertYbrToRgb(uint8_t rgb[3],
+                              const uint8_t ybr[3])
+  {
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.3.html#sect_C.7.6.3.1.2
+    // https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion
+    
+    // TODO - Check out the outcome of Mathieu's discussion about
+    // truncation of YCbCr-to-RGB conversion:
+    // https://groups.google.com/forum/#!msg/comp.protocols.dicom/JHuGeyWbTz8/ARoTWrJzAQAJ
+
+    const float Y  = ybr[0];
+    const float Cb = ybr[1];
+    const float Cr = ybr[2];
+
+    const float result[3] = {
+      Y                             + 1.402f    * (Cr - 128.0f),
+      Y - 0.344136f * (Cb - 128.0f) - 0.714136f * (Cr - 128.0f),
+      Y + 1.772f    * (Cb - 128.0f)
+    };
+
+    for (uint8_t i = 0; i < 3 ; i++)
+    {
+      if (result[i] < 0)
+      {
+        rgb[i] = 0;
+      }
+      else if (result[i] > 255)
+      {
+        rgb[i] = 255;
+      }
+      else
+      {
+        rgb[i] = static_cast<uint8_t>(result[i]);
+      }
+    }    
+  }
+
+  
+  static void FixPhotometricInterpretation(OrthancImageWrapper& image,
+                                           gdcm::PhotometricInterpretation interpretation)
+  {
+    switch (interpretation)
+    {
+      case gdcm::PhotometricInterpretation::MONOCHROME1:
+      case gdcm::PhotometricInterpretation::MONOCHROME2:
+      case gdcm::PhotometricInterpretation::RGB:
+        return;
+
+      case gdcm::PhotometricInterpretation::YBR_FULL:
+      {
+        // Fix for Osimis issue WVB-319: Some images are not loading in US_MF
+
+        uint32_t width = image.GetWidth();
+        uint32_t height = image.GetHeight();
+        uint32_t pitch = image.GetPitch();
+        uint8_t* buffer = reinterpret_cast<uint8_t*>(image.GetBuffer());
+        
+        if (image.GetFormat() != OrthancPluginPixelFormat_RGB24 ||
+            pitch < 3 * width)
+        {
+          throw std::runtime_error("Internal error");
+        }
+
+        for (uint32_t y = 0; y < height; y++)
+        {
+          uint8_t* p = buffer + y * pitch;
+          for (uint32_t x = 0; x < width; x++, p += 3)
+          {
+            const uint8_t ybr[3] = { p[0], p[1], p[2] };
+            uint8_t rgb[3];
+            ConvertYbrToRgb(rgb, ybr);
+            p[0] = rgb[0];
+            p[1] = rgb[1];
+            p[2] = rgb[2];
+          }
+        }
+
+        return;
+      }
+
+      default:
+        throw std::runtime_error("Unsupported output photometric interpretation");
+    }    
+  }
+
+
+  OrthancPluginImage* GdcmImageDecoder::Decode(OrthancPluginContext* context,
+                                               unsigned int frameIndex) const
+  {
+    unsigned int frames = GetFramesCount();
+    unsigned int width = GetWidth();
+    unsigned int height = GetHeight();
+    OrthancPluginPixelFormat format = GetFormat();
+    size_t bpp = GetBytesPerPixel(format);
+
+    if (frameIndex >= frames)
+    {
+      throw std::runtime_error("Inexistent frame index");
+    }
+
+    std::string& decoded = pimpl_->decoded_;
+    OrthancImageWrapper target(context, format, width, height);
+
+    if (width == 0 ||
+        height == 0)
+    {
+      return target.Release();
+    }
+
+    if (decoded.empty())
+    {
+      decoded.resize(pimpl_->GetImage().GetBufferLength());
+      pimpl_->GetImage().GetBuffer(&decoded[0]);
+    }
+
+    const void* sourceBuffer = &decoded[0];
+
+    if (target.GetPitch() == bpp * width &&
+        frames == 1)
+    {
+      assert(decoded.size() == target.GetPitch() * target.GetHeight());      
+      memcpy(target.GetBuffer(), sourceBuffer, decoded.size());
+    }
+    else 
+    {
+      size_t targetPitch = target.GetPitch();
+      size_t sourcePitch = width * bpp;
+
+      const char* a = &decoded[sourcePitch * height * frameIndex];
+      char* b = target.GetBuffer();
+
+      for (uint32_t y = 0; y < height; y++)
+      {
+        memcpy(b, a, sourcePitch);
+        a += sourcePitch;
+        b += targetPitch;
+      }
+    }
+    
+    FixPhotometricInterpretation(target, pimpl_->GetImage().GetPhotometricInterpretation());
+                                 
+    return target.Release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <orthanc/OrthancCPlugin.h>
+#include <stdint.h>
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+
+
+// This is for compatibility with Orthanc SDK <= 1.3.0
+#if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
+#define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision) \
+  (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major ||               \
+   (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major &&             \
+    (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor ||             \
+     (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor &&           \
+      ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision))))
+#endif
+
+
+namespace OrthancPlugins
+{
+  class GdcmImageDecoder : public boost::noncopyable
+  {
+  private:
+    struct PImpl;
+    boost::shared_ptr<PImpl> pimpl_;
+  
+  public:
+    GdcmImageDecoder(const void* dicom,
+                     size_t size);
+
+    OrthancPluginPixelFormat GetFormat() const;
+
+    unsigned int GetWidth() const;
+
+    unsigned int GetHeight() const;
+
+    unsigned int GetFramesCount() const;
+
+    static size_t GetBytesPerPixel(OrthancPluginPixelFormat format);
+
+    OrthancPluginImage* Decode(OrthancPluginContext* context,
+                               unsigned int frameIndex) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/GdcmDecoder/OrthancImageWrapper.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,100 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OrthancImageWrapper.h"
+
+#include <stdexcept>
+
+namespace OrthancPlugins
+{
+  OrthancImageWrapper::OrthancImageWrapper(OrthancPluginContext* context,
+                                           OrthancPluginPixelFormat format,
+                                           uint32_t width,
+                                           uint32_t height) :
+    context_(context)
+  {
+    image_ = OrthancPluginCreateImage(context_, format, width, height);
+    if (image_ == NULL)
+    {
+      throw std::runtime_error("Cannot create an image");
+    }
+  }
+
+
+  OrthancImageWrapper::OrthancImageWrapper(OrthancPluginContext* context,
+                                           OrthancPluginImage* image) :
+    context_(context),
+    image_(image)
+  {
+    if (image_ == NULL)
+    {
+      throw std::runtime_error("Invalid image returned by the core of Orthanc");
+    }
+  }
+
+
+
+  OrthancImageWrapper::~OrthancImageWrapper()
+  {
+    if (image_ != NULL)
+    {
+      OrthancPluginFreeImage(context_, image_);
+    }
+  }
+
+
+  OrthancPluginImage* OrthancImageWrapper::Release()
+  {
+    OrthancPluginImage* tmp = image_;
+    image_ = NULL;
+    return tmp;
+  }
+
+
+  uint32_t OrthancImageWrapper::GetWidth()
+  {
+    return OrthancPluginGetImageWidth(context_, image_);
+  }
+
+
+  uint32_t OrthancImageWrapper::GetHeight()
+  {
+    return OrthancPluginGetImageHeight(context_, image_);
+  }
+
+
+  uint32_t OrthancImageWrapper::GetPitch()
+  {
+    return OrthancPluginGetImagePitch(context_, image_);
+  }
+
+
+  OrthancPluginPixelFormat OrthancImageWrapper::GetFormat()
+  {
+    return OrthancPluginGetImagePixelFormat(context_, image_);
+  }
+
+
+  char* OrthancImageWrapper::GetBuffer()
+  {
+    return reinterpret_cast<char*>(OrthancPluginGetImageBuffer(context_, image_));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/GdcmDecoder/OrthancImageWrapper.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,64 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <orthanc/OrthancCPlugin.h>
+
+#include "GdcmImageDecoder.h"
+
+namespace OrthancPlugins
+{
+  class OrthancImageWrapper
+  {
+  private:
+    OrthancPluginContext*  context_;
+    OrthancPluginImage*    image_;
+
+  public:
+    OrthancImageWrapper(OrthancPluginContext* context,
+                        OrthancPluginPixelFormat format,
+                        uint32_t width,
+                        uint32_t height);
+
+    OrthancImageWrapper(OrthancPluginContext* context,
+                        OrthancPluginImage* image);  // Takes ownership
+
+    ~OrthancImageWrapper();
+
+    OrthancPluginContext* GetContext()
+    {
+      return context_;
+    }
+
+    OrthancPluginImage* Release();
+
+    uint32_t GetWidth();
+
+    uint32_t GetHeight();
+
+    uint32_t GetPitch();
+
+    OrthancPluginPixelFormat GetFormat();
+
+    char* GetBuffer();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/GdcmDecoder/Plugin.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,107 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "GdcmDecoderCache.h"
+#include "OrthancImageWrapper.h"
+
+#include <orthanc/OrthancCPlugin.h>
+
+static OrthancPluginContext* context_ = NULL;
+static OrthancPlugins::GdcmDecoderCache  cache_;
+
+
+static OrthancPluginErrorCode DecodeImageCallback(OrthancPluginImage** target,
+                                                  const void* dicom,
+                                                  const uint32_t size,
+                                                  uint32_t frameIndex)
+{
+  try
+  {
+    std::auto_ptr<OrthancPlugins::OrthancImageWrapper> image;
+
+#if 0
+    // Do not use the cache
+    OrthancPlugins::GdcmImageDecoder decoder(dicom, size);
+    image.reset(new OrthancPlugins::OrthancImageWrapper(context_, decoder.Decode(context_, frameIndex)));
+#else
+    image.reset(cache_.Decode(context_, dicom, size, frameIndex));
+#endif
+
+    *target = image->Release();
+
+    return OrthancPluginErrorCode_Success;
+  }
+  catch (std::runtime_error& e)
+  {
+    *target = NULL;
+
+    std::string s = "Cannot decode image using GDCM: " + std::string(e.what());
+    OrthancPluginLogInfo(context_, s.c_str());
+    return OrthancPluginErrorCode_Plugin;
+  }
+}
+
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    context_ = context;
+    OrthancPluginLogWarning(context_, "Initializing the advanced decoder of medical images using GDCM");
+
+
+    /* 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;
+    }
+
+    OrthancPluginSetDescription(context_, "Advanced decoder of medical images using GDCM.");
+    OrthancPluginRegisterDecodeImageCallback(context_, DecodeImageCallback);
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "gdcm-decoder";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return GDCM_DECODER_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/GdcmDecoder/README	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,6 @@
+This sample shows how to replace the decoder of DICOM images that is
+built in Orthanc, by the GDCM library.
+
+A production-ready version of this sample, is available in the
+offical Web viewer plugin:
+http://www.orthanc-server.com/static.php?page=web-viewer
--- a/Plugins/Samples/GdcmDecoding/CMakeLists.txt	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-cmake_minimum_required(VERSION 2.8)
-
-project(GdcmDecoding)
-
-SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages")
-SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
-
-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(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
-include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
-
-include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake)
-
-find_package(GDCM REQUIRED)
-if (GDCM_FOUND)
-  include(${GDCM_USE_FILE})
-  set(GDCM_LIBRARIES gdcmCommon gdcmMSFF)
-else(GDCM_FOUND)
-  message(FATAL_ERROR "Cannot find GDCM, did you set GDCM_DIR?")
-endif(GDCM_FOUND)
-
-add_library(GdcmDecoding SHARED
-  Plugin.cpp
-  OrthancContext.cpp
-
-  # Sources from Orthanc
-  ${GOOGLE_LOG_SOURCES}
-  ${ORTHANC_ROOT}/Core/ChunkedBuffer.cpp
-  ${ORTHANC_ROOT}/Core/Enumerations.cpp
-  ${ORTHANC_ROOT}/Core/Images/ImageAccessor.cpp
-  ${ORTHANC_ROOT}/Core/Images/ImageBuffer.cpp
-  ${ORTHANC_ROOT}/Core/Images/ImageProcessing.cpp
-  ${ORTHANC_ROOT}/Core/Toolbox.cpp
-  ${ORTHANC_ROOT}/Resources/ThirdParty/base64/base64.cpp
-  ${ORTHANC_ROOT}/Resources/ThirdParty/md5/md5.c
-  ${JSONCPP_SOURCES}
-  ${THIRD_PARTY_SOURCES}
-  )
-
-target_link_libraries(GdcmDecoding ${GDCM_LIBRARIES})
--- a/Plugins/Samples/GdcmDecoding/OrthancContext.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,151 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "OrthancContext.h"
-
-#include <stdexcept>
-
-
-void OrthancContext::Check()
-{
-  if (context_ == NULL)
-  {
-    throw std::runtime_error("The Orthanc plugin context is not initialized");
-  }
-}
-
-
-OrthancContext& OrthancContext::GetInstance()
-{
-  static OrthancContext instance;
-  return instance;
-}
-
-
-void OrthancContext::ExtractGetArguments(Arguments& arguments,
-                                         const OrthancPluginHttpRequest& request)
-{
-  Check();
-  arguments.clear();
-
-  for (uint32_t i = 0; i < request.getCount; i++)
-  {
-    arguments[request.getKeys[i]] = request.getValues[i];
-  }
-}
-
-
-void OrthancContext::LogError(const std::string& s)
-{
-  Check();
-  OrthancPluginLogError(context_, s.c_str());
-}
-
-
-void OrthancContext::LogWarning(const std::string& s)
-{
-  Check();
-  OrthancPluginLogWarning(context_, s.c_str());
-}
-
-
-void OrthancContext::LogInfo(const std::string& s)
-{
-  Check();
-  OrthancPluginLogInfo(context_, s.c_str());
-}
-  
-
-void OrthancContext::Register(const std::string& uri,
-                              OrthancPluginRestCallback callback)
-{
-  Check();
-  OrthancPluginRegisterRestCallback(context_, uri.c_str(), callback);
-}
-
-
-void OrthancContext::GetDicomForInstance(std::string& result,
-                                         const std::string& instanceId)
-{
-  Check();
-  OrthancPluginMemoryBuffer buffer;
-    
-  if (OrthancPluginGetDicomForInstance(context_, &buffer, instanceId.c_str()))
-  {
-    throw std::runtime_error("No DICOM instance with Orthanc ID: " + instanceId);
-  }
-
-  if (buffer.size == 0)
-  {
-    result.clear();
-  }
-  else
-  {
-    result.assign(reinterpret_cast<char*>(buffer.data), buffer.size);
-  }
-
-  OrthancPluginFreeMemoryBuffer(context_, &buffer);
-}
-
-
-void OrthancContext::CompressAndAnswerPngImage(OrthancPluginRestOutput* output,
-                                               const Orthanc::ImageAccessor& accessor)
-{
-  Check();
-
-  OrthancPluginPixelFormat format;
-  switch (accessor.GetFormat())
-  {
-    case Orthanc::PixelFormat_Grayscale8:
-      format = OrthancPluginPixelFormat_Grayscale8;
-      break;
-
-    case Orthanc::PixelFormat_Grayscale16:
-      format = OrthancPluginPixelFormat_Grayscale16;
-      break;
-
-    case Orthanc::PixelFormat_SignedGrayscale16:
-      format = OrthancPluginPixelFormat_SignedGrayscale16;
-      break;
-
-    case Orthanc::PixelFormat_RGB24:
-      format = OrthancPluginPixelFormat_RGB24;
-      break;
-
-    case Orthanc::PixelFormat_RGBA32:
-      format = OrthancPluginPixelFormat_RGBA32;
-      break;
-
-    default:
-      throw std::runtime_error("Unsupported pixel format");
-  }
-
-  OrthancPluginCompressAndAnswerPngImage(context_, output, format, accessor.GetWidth(),
-                                         accessor.GetHeight(), accessor.GetPitch(), accessor.GetConstBuffer());
-}
-
-
-
-void OrthancContext::Redirect(OrthancPluginRestOutput* output,
-                              const std::string& s)
-{
-  Check();
-  OrthancPluginRedirect(context_, output, s.c_str());
-}
--- a/Plugins/Samples/GdcmDecoding/OrthancContext.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include <orthanc/OrthancCPlugin.h>
-
-#include "../../../Core/Images/ImageBuffer.h"
-
-#include <map>
-#include <string>
-#include <boost/noncopyable.hpp>
-
-
-class OrthancContext : public boost::noncopyable
-{
-private:
-  OrthancPluginContext* context_;
-
-  OrthancContext() : context_(NULL)
-  {
-  }
-
-  void Check();
-
-public:
-  typedef std::map<std::string, std::string>  Arguments;
-
-  static OrthancContext& GetInstance();
-
-  void Initialize(OrthancPluginContext* context)
-  {
-    context_ = context;
-  }
-
-  void Finalize()
-  {
-    context_ = NULL;
-  }
-
-  void ExtractGetArguments(Arguments& arguments,
-                           const OrthancPluginHttpRequest& request);
-
-  void LogError(const std::string& s);
-
-  void LogWarning(const std::string& s);
-
-  void LogInfo(const std::string& s);
-  
-  void Register(const std::string& uri,
-                OrthancPluginRestCallback callback);
-
-  void GetDicomForInstance(std::string& result,
-                           const std::string& instanceId);
-
-  void CompressAndAnswerPngImage(OrthancPluginRestOutput* output,
-                                 const Orthanc::ImageAccessor& accessor);
-
-  void Redirect(OrthancPluginRestOutput* output,
-                const std::string& s);
-};
--- a/Plugins/Samples/GdcmDecoding/Plugin.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,264 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include <map>
-#include <string>
-#include <stdexcept>
-#include <sstream>
-#include <boost/lexical_cast.hpp>
-
-#include "OrthancContext.h"
-#include "../../../Core/Images/ImageProcessing.h"
-
-#include <gdcmReader.h>
-#include <gdcmImageReader.h>
-#include <gdcmImageChangePlanarConfiguration.h>
-
-
-static void AnswerUnsupportedImage(OrthancPluginRestOutput* output)
-{
-  OrthancContext::GetInstance().Redirect(output, "/app/images/unsupported.png");
-}
-
-
-static bool GetOrthancPixelFormat(Orthanc::PixelFormat& format,
-                                  const gdcm::Image& image)
-{
-  if (image.GetPlanarConfiguration() != 0 && 
-      image.GetPixelFormat().GetSamplesPerPixel() != 1)
-  {
-    OrthancContext::GetInstance().LogError("Planar configurations are not supported");
-    return false;
-  }
-
-  if (image.GetPixelFormat().GetSamplesPerPixel() == 1)
-  {
-    switch (image.GetPixelFormat().GetScalarType())
-    {
-      case gdcm::PixelFormat::UINT8:
-        format = Orthanc::PixelFormat_Grayscale8;
-        return true;
-
-      case gdcm::PixelFormat::UINT16:
-        format = Orthanc::PixelFormat_Grayscale16;
-        return true;
-
-      case gdcm::PixelFormat::INT16:
-        format = Orthanc::PixelFormat_SignedGrayscale16;
-        return true;
-
-      default:
-        return false;
-    }
-  }
-  else if (image.GetPixelFormat().GetSamplesPerPixel() == 3 &&
-           image.GetPixelFormat().GetScalarType() == gdcm::PixelFormat::UINT8)
-  {
-    format = Orthanc::PixelFormat_RGB24;
-    return true;
-  }
-  else if (image.GetPixelFormat().GetSamplesPerPixel() == 4 &&
-           image.GetPixelFormat().GetScalarType() == gdcm::PixelFormat::UINT8)
-  {
-    format = Orthanc::PixelFormat_RGBA32;
-    return true;
-  }
-  
-  return false;
-}
-
-
-ORTHANC_PLUGINS_API OrthancPluginErrorCode DecodeImage(OrthancPluginRestOutput* output,
-                                                       const char* url,
-                                                       const OrthancPluginHttpRequest* request)
-{
-  std::string instance(request->groups[0]);
-  std::string outputFormat(request->groups[1]);
-  OrthancContext::GetInstance().LogWarning("Using GDCM to decode instance " + instance);
-
-  // Download the request DICOM instance from Orthanc into a memory buffer
-  std::string dicom;
-  OrthancContext::GetInstance().GetDicomForInstance(dicom, instance);
-
-  // Prepare a memory stream over the DICOM instance
-  std::stringstream stream(dicom);
-
-  // Parse the DICOM instance using GDCM
-  gdcm::ImageReader imageReader;
-  imageReader.SetStream(stream);
-  if (!imageReader.Read())
-  {
-    OrthancContext::GetInstance().LogError("GDCM cannot extract an image from this DICOM instance");
-    AnswerUnsupportedImage(output);
-    return OrthancPluginErrorCode_Success;
-  }
-
-  gdcm::Image& image = imageReader.GetImage();
-
-
-  // Log information about the decoded image
-  char tmp[1024];
-  sprintf(tmp, "Image format: %dx%d %s with %d color channel(s)", image.GetRows(), image.GetColumns(), 
-          image.GetPixelFormat().GetScalarTypeAsString(), image.GetPixelFormat().GetSamplesPerPixel());
-  OrthancContext::GetInstance().LogWarning(tmp);
-
-
-  // Convert planar configuration
-  gdcm::ImageChangePlanarConfiguration planar;
-  if (image.GetPlanarConfiguration() != 0 && 
-      image.GetPixelFormat().GetSamplesPerPixel() != 1)
-  {
-    OrthancContext::GetInstance().LogWarning("Converting planar configuration to interleaved");
-    planar.SetInput(imageReader.GetImage());
-    planar.Change();
-    image = planar.GetOutput();
-  }
-
-
-  // Create a read-only accessor to the bitmap decoded by GDCM
-  Orthanc::PixelFormat format;
-  if (!GetOrthancPixelFormat(format, image))
-  {
-    OrthancContext::GetInstance().LogError("This sample plugin does not support this image format");
-    AnswerUnsupportedImage(output);
-    return OrthancPluginErrorCode_Success;
-  }
-
-  Orthanc::ImageAccessor decodedImage;
-  std::vector<char> decodedBuffer(image.GetBufferLength());
-
-  if (decodedBuffer.size())
-  {
-    image.GetBuffer(&decodedBuffer[0]);
-    unsigned int pitch = image.GetColumns() * ::Orthanc::GetBytesPerPixel(format);
-    decodedImage.AssignWritable(format, image.GetColumns(), image.GetRows(), pitch, &decodedBuffer[0]);
-  }
-  else
-  {
-    // Empty image
-    decodedImage.AssignWritable(format, 0, 0, 0, NULL);
-  }
-
-
-  // Convert the pixel format from GDCM to the format requested by the REST query
-  Orthanc::ImageBuffer converted;
-  converted.SetWidth(decodedImage.GetWidth());
-  converted.SetHeight(decodedImage.GetHeight());
-
-  if (outputFormat == "preview")
-  {
-    if (format == Orthanc::PixelFormat_RGB24 ||
-        format == Orthanc::PixelFormat_RGBA32)
-    {
-      // Do not rescale color image
-      converted.SetFormat(Orthanc::PixelFormat_RGB24);
-    }
-    else
-    {
-      converted.SetFormat(Orthanc::PixelFormat_Grayscale8);
-
-      // Rescale the image to the [0,255] range
-      int64_t a, b;
-      Orthanc::ImageProcessing::GetMinMaxValue(a, b, decodedImage);
-
-      float offset = -a;
-      float scaling = 255.0f / static_cast<float>(b - a);
-      Orthanc::ImageProcessing::ShiftScale(decodedImage, offset, scaling);
-    }
-  }
-  else
-  {
-    if (format == Orthanc::PixelFormat_RGB24 ||
-        format == Orthanc::PixelFormat_RGBA32)
-    {
-      // Do not convert color images to grayscale values (this is Orthanc convention)
-      AnswerUnsupportedImage(output);
-      return OrthancPluginErrorCode_Success;
-    }
-
-    if (outputFormat == "image-uint8")
-    {
-      converted.SetFormat(Orthanc::PixelFormat_Grayscale8);
-    }
-    else if (outputFormat == "image-uint16")
-    {
-      converted.SetFormat(Orthanc::PixelFormat_Grayscale16);
-    }
-    else if (outputFormat == "image-int16")
-    {
-      converted.SetFormat(Orthanc::PixelFormat_SignedGrayscale16);
-    }
-    else
-    {
-      OrthancContext::GetInstance().LogError("Unknown output format: " + outputFormat);
-      AnswerUnsupportedImage(output);
-      return OrthancPluginErrorCode_Success;
-    }
-  }
-
-  Orthanc::ImageAccessor convertedAccessor(converted.GetAccessor());
-  Orthanc::ImageProcessing::Convert(convertedAccessor, decodedImage);
-
-  // Compress the converted image as a PNG file
-  OrthancContext::GetInstance().CompressAndAnswerPngImage(output, convertedAccessor);
-
-  return OrthancPluginErrorCode_Success;  // Success
-}
-
-
-extern "C"
-{
-  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
-  {
-    OrthancContext::GetInstance().Initialize(context);
-    OrthancContext::GetInstance().LogWarning("Initializing GDCM decoding");
-
-    // Check the version of the Orthanc core
-    if (OrthancPluginCheckVersion(context) == 0)
-    {
-      OrthancContext::GetInstance().LogError(
-        "Your version of Orthanc (" + std::string(context->orthancVersion) +
-        ") must be above " + boost::lexical_cast<std::string>(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER) +
-        "." + boost::lexical_cast<std::string>(ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER) +
-        "." + boost::lexical_cast<std::string>(ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER) +
-        " to run this plugin");
-      return -1;
-    }
-
-    OrthancContext::GetInstance().Register("/instances/([^/]+)/(preview|image-uint8|image-uint16|image-int16)", DecodeImage);
-    return 0;
-  }
-
-  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
-  {
-    OrthancContext::GetInstance().LogWarning("Finalizing GDCM decoding");
-    OrthancContext::GetInstance().Finalize();
-  }
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
-  {
-    return "gdcm-decoding";
-  }
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
-  {
-    return "1.0";
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/ModalityWorklists/CMakeLists.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,37 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(ModalityWorklists)
+
+SET(MODALITY_WORKLISTS_VERSION "0.0" CACHE STRING "Version of the plugin")
+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")
+
+SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp")
+SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost")
+
+set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
+include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
+
+add_library(ModalityWorklists SHARED 
+  Plugin.cpp
+  ../Common/OrthancPluginCppWrapper.cpp
+  ${JSONCPP_SOURCES}
+  ${BOOST_SOURCES}
+  )
+
+message("Setting the version of the plugin to ${MODALITY_WORKLISTS_VERSION}")
+add_definitions(
+  -DMODALITY_WORKLISTS_VERSION="${MODALITY_WORKLISTS_VERSION}"
+  )
+
+set_target_properties(ModalityWorklists PROPERTIES 
+  VERSION ${MODALITY_WORKLISTS_VERSION} 
+  SOVERSION ${MODALITY_WORKLISTS_VERSION})
+
+install(
+  TARGETS ModalityWorklists
+  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/Plugins/Samples/ModalityWorklists/Plugin.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,268 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+#include <boost/filesystem.hpp>
+#include <json/value.h>
+#include <json/reader.h>
+#include <string.h>
+#include <iostream>
+#include <algorithm>
+
+static OrthancPluginContext* context_ = NULL;
+static std::string folder_;
+static bool filterIssuerAet_ = false;
+
+/**
+ * This is the main function for matching a DICOM worklist against a query.
+ **/
+static bool MatchWorklist(OrthancPluginWorklistAnswers*      answers,
+                           const OrthancPluginWorklistQuery*  query,
+                           const OrthancPlugins::FindMatcher& matcher,
+                           const std::string& path)
+{
+  OrthancPlugins::MemoryBuffer dicom(context_);
+  dicom.ReadFile(path);
+
+  if (matcher.IsMatch(dicom))
+  {
+    // This DICOM file matches the worklist query, add it to the answers
+    OrthancPluginErrorCode code = OrthancPluginWorklistAddAnswer
+      (context_, answers, query, dicom.GetData(), dicom.GetSize());
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      OrthancPlugins::LogError(context_, "Error while adding an answer to a worklist request");
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+
+    return true;
+  }
+
+  return false;
+}
+
+
+static OrthancPlugins::FindMatcher* CreateMatcher(const OrthancPluginWorklistQuery* query,
+                                                  const char*                       issuerAet)
+{
+  // Extract the DICOM instance underlying the C-Find query
+  OrthancPlugins::MemoryBuffer dicom(context_);
+  dicom.GetDicomQuery(query);
+
+  // Convert the DICOM as JSON, and dump it to the user in "--verbose" mode
+  Json::Value json;
+  dicom.DicomToJson(json, OrthancPluginDicomToJsonFormat_Short,
+                    static_cast<OrthancPluginDicomToJsonFlags>(0), 0);
+
+  OrthancPlugins::LogInfo(context_, "Received worklist query from remote modality " +
+                          std::string(issuerAet) + ":\n" + json.toStyledString());
+
+  if (!filterIssuerAet_)
+  {
+    return new OrthancPlugins::FindMatcher(context_, query);
+  }
+  else
+  {
+    // Alternative sample showing how to fine-tune an incoming C-Find
+    // request, before matching it against the worklist database. The
+    // code below will restrict the original DICOM request by
+    // requesting the ScheduledStationAETitle to correspond to the AET
+    // of the C-Find issuer. This code will make the integration test
+    // "test_filter_issuer_aet" succeed (cf. the orthanc-tests repository).
+
+    static const char* SCHEDULED_PROCEDURE_STEP_SEQUENCE = "0040,0100";
+    static const char* SCHEDULED_STATION_AETITLE = "0040,0001";
+    static const char* PREGNANCY_STATUS = "0010,21c0";
+
+    if (!json.isMember(SCHEDULED_PROCEDURE_STEP_SEQUENCE))
+    {
+      // Create a ScheduledProcedureStepSequence sequence, with one empty element
+      json[SCHEDULED_PROCEDURE_STEP_SEQUENCE] = Json::arrayValue;
+      json[SCHEDULED_PROCEDURE_STEP_SEQUENCE].append(Json::objectValue);
+    }
+
+    Json::Value& v = json[SCHEDULED_PROCEDURE_STEP_SEQUENCE];
+
+    if (v.type() != Json::arrayValue ||
+        v.size() != 1 ||
+        v[0].type() != Json::objectValue)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+
+    // Set the ScheduledStationAETitle if none was provided
+    if (!v[0].isMember(SCHEDULED_STATION_AETITLE) ||
+        v[0].type() != Json::stringValue ||
+        v[0][SCHEDULED_STATION_AETITLE].asString().size() == 0 ||
+        v[0][SCHEDULED_STATION_AETITLE].asString() == "*")
+    {
+      v[0][SCHEDULED_STATION_AETITLE] = issuerAet;
+    }
+
+    if (json.isMember(PREGNANCY_STATUS) &&
+        json[PREGNANCY_STATUS].asString().size() == 0)
+    {
+      json.removeMember(PREGNANCY_STATUS);
+    }
+
+    // Encode the modified JSON as a DICOM instance, then convert it to a C-Find matcher
+    OrthancPlugins::MemoryBuffer modified(context_);
+    modified.CreateDicom(json, OrthancPluginCreateDicomFlags_None);
+    return new OrthancPlugins::FindMatcher(context_, modified);
+  }
+}
+
+
+
+OrthancPluginErrorCode Callback(OrthancPluginWorklistAnswers*     answers,
+                                const OrthancPluginWorklistQuery* query,
+                                const char*                       issuerAet,
+                                const char*                       calledAet)
+{
+  try
+  {
+    // Construct an object to match the worklists in the database against the C-Find query
+    std::auto_ptr<OrthancPlugins::FindMatcher> matcher(CreateMatcher(query, issuerAet));
+
+    // Loop over the regular files in the database folder
+    namespace fs = boost::filesystem;
+
+    fs::path source(folder_);
+    fs::directory_iterator end;
+    int parsedFilesCount = 0;
+    int matchedWorklistCount = 0;
+
+    try
+    {
+      for (fs::directory_iterator it(source); it != end; ++it)
+      {
+        fs::file_type type(it->status().type());
+
+        if (type == fs::regular_file ||
+            type == fs::reparse_file)   // cf. BitBucket issue #11
+        {
+          std::string extension = fs::extension(it->path());
+          std::transform(extension.begin(), extension.end(), extension.begin(), tolower);  // Convert to lowercase
+
+          if (extension == ".wl")
+          {
+            parsedFilesCount++;
+            // We found a worklist (i.e. a DICOM find with extension ".wl"), match it against the query
+            if (MatchWorklist(answers, query, *matcher, it->path().string()))
+            {
+              OrthancPlugins::LogInfo(context_, "Worklist matched: " + it->path().string());
+              matchedWorklistCount++;
+            }
+          }
+        }
+      }
+
+      std::ostringstream message;
+      message << "Worklist C-Find: parsed " << parsedFilesCount << " files, found " << matchedWorklistCount << " match(es)";
+      OrthancPlugins::LogInfo(context_, message.str());
+
+    }
+    catch (fs::filesystem_error&)
+    {
+      OrthancPlugins::LogError(context_, "Inexistent folder while scanning for worklists: " + source.string());
+      return OrthancPluginErrorCode_DirectoryExpected;
+    }
+
+    // Uncomment the following line if too many answers are to be returned
+    // OrthancPluginMarkWorklistAnswersIncomplete(context_, answers);
+
+    return OrthancPluginErrorCode_Success;
+  }
+  catch (OrthancPlugins::PluginException& e)
+  {
+    return e.GetErrorCode();
+  }
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    context_ = c;
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(c) == 0)
+    {
+      OrthancPlugins::ReportMinimalOrthancVersion(context_,
+                                                  ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      return -1;
+    }
+
+    OrthancPlugins::LogWarning(context_, "Sample worklist plugin is initializing");
+    OrthancPluginSetDescription(context_, "Serve DICOM modality worklists from a folder with Orthanc.");
+
+    OrthancPlugins::OrthancConfiguration configuration(context_);
+
+    OrthancPlugins::OrthancConfiguration worklists;
+    configuration.GetSection(worklists, "Worklists");
+
+    bool enabled = worklists.GetBooleanValue("Enable", false);
+    if (enabled)
+    {
+      if (worklists.LookupStringValue(folder_, "Database"))
+      {
+        OrthancPlugins::LogWarning(context_, "The database of worklists will be read from folder: " + folder_);
+        OrthancPluginRegisterWorklistCallback(context_, Callback);
+      }
+      else
+      {
+        OrthancPlugins::LogError(context_, "The configuration option \"Worklists.Database\" must contain a path");
+        return -1;
+      }
+
+      filterIssuerAet_ = worklists.GetBooleanValue("FilterIssuerAet", false);
+    }
+    else
+    {
+      OrthancPlugins::LogWarning(context_, "Worklist server is disabled by the configuration file");
+    }
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    OrthancPluginLogWarning(context_, "Sample worklist plugin is finalizing");
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "worklists";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return MODALITY_WORKLISTS_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/ModalityWorklists/README	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,11 @@
+Introduction
+============
+
+This sample plugin enables Orthanc to serve DICOM modality worklists
+that are read from some folder.
+
+The shared library containing the plugin is created as part of the
+build process of Orthanc.
+
+Documentation is available in the Orthanc Book:
+http://book.orthanc-server.com/plugins/worklists-plugin.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/ModalityWorklists/WorklistsDatabase/Generate.py	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+
+import os
+import subprocess
+
+SOURCE = '/home/jodogne/Downloads/dcmtk-3.6.0/dcmwlm/data/wlistdb/OFFIS/'
+TARGET = os.path.abspath(os.path.dirname(__file__))
+
+for f in os.listdir(SOURCE):
+    ext = os.path.splitext(f)
+
+    if ext[1].lower() == '.dump':
+        subprocess.check_call([
+            'dump2dcm',
+            '-g',
+            '-q',
+            os.path.join(SOURCE, f),
+            os.path.join(TARGET, ext[0].lower() + '.wl'),
+        ])
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist1.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist10.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist2.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist3.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist4.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist5.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist6.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist7.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist8.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist9.wl has changed
--- a/Plugins/Samples/ServeFolders/CMakeLists.txt	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Samples/ServeFolders/CMakeLists.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -16,10 +16,13 @@
 
 add_library(ServeFolders SHARED 
   Plugin.cpp
+  ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp
   ${JSONCPP_SOURCES}
   ${BOOST_SOURCES}
   )
 
+add_definitions(-DHAS_ORTHANC_EXCEPTION=0)
+
 message("Setting the version of the plugin to ${SERVE_FOLDERS_VERSION}")
 add_definitions(-DSERVE_FOLDERS_VERSION="${SERVE_FOLDERS_VERSION}")
 
--- a/Plugins/Samples/ServeFolders/Plugin.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Samples/ServeFolders/Plugin.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -18,122 +19,82 @@
  **/
 
 
-#include <orthanc/OrthancCPlugin.h>
+#include "../Common/OrthancPluginCppWrapper.h"
 
 #include <json/reader.h>
 #include <json/value.h>
 #include <boost/filesystem.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+
+#if HAS_ORTHANC_EXCEPTION == 1
+#  error The macro HAS_ORTHANC_EXCEPTION must be set to 0 to compile this plugin
+#endif
+
 
 
 static OrthancPluginContext* context_ = NULL;
+static std::map<std::string, std::string> extensions_;
 static std::map<std::string, std::string> folders_;
 static const char* INDEX_URI = "/app/plugin-serve-folders.html";
+static bool allowCache_ = false;
+static bool generateETag_ = true;
 
 
-static const char* GetMimeType(const std::string& path)
+static void SetHttpHeaders(OrthancPluginRestOutput* output)
 {
-  size_t dot = path.find_last_of('.');
-
-  std::string extension = (dot == std::string::npos) ? "" : path.substr(dot);
-  std::transform(extension.begin(), extension.end(), extension.begin(), tolower);
-
-  if (extension == ".html")
-  {
-    return "text/html";
-  }
-  else if (extension == ".css")
-  {
-    return "text/css";
-  }
-  else if (extension == ".js")
-  {
-    return "application/javascript";
-  }
-  else if (extension == ".gif")
-  {
-    return "image/gif";
-  }
-  else if (extension == ".svg")
+  if (!allowCache_)
   {
-    return "image/svg+xml";
-  }
-  else if (extension == ".json")
-  {
-    return "application/json";
-  }
-  else if (extension == ".xml")
-  {
-    return "application/xml";
-  }
-  else if (extension == ".png")
-  {
-    return "image/png";
-  }
-  else if (extension == ".jpg" || extension == ".jpeg")
-  {
-    return "image/jpeg";
-  }
-  else if (extension == ".woff")
-  {
-    return "application/x-font-woff";
-  }
-  else
-  {
-    std::string s = "Unknown MIME type for extension: " + extension;
-    OrthancPluginLogWarning(context_, s.c_str());
-    return "application/octet-stream";
+    // http://stackoverflow.com/a/2068407/881731
+    OrthancPluginSetHttpHeader(context_, output, "Cache-Control", "no-cache, no-store, must-revalidate");
+    OrthancPluginSetHttpHeader(context_, output, "Pragma", "no-cache");
+    OrthancPluginSetHttpHeader(context_, output, "Expires", "0");
   }
 }
 
 
-static bool ReadFile(std::string& target,
-                     const std::string& path)
+static void RegisterDefaultExtensions()
 {
-  OrthancPluginMemoryBuffer buffer;
-  if (OrthancPluginReadFile(context_, &buffer, path.c_str()))
-  {
-    return false;
-  }
-  else
-  {
-    target.assign(reinterpret_cast<const char*>(buffer.data), buffer.size);
-    OrthancPluginFreeMemoryBuffer(context_, &buffer);
-    return true;
-  }
+  extensions_["css"]  = "text/css";
+  extensions_["gif"]  = "image/gif";
+  extensions_["html"] = "text/html";
+  extensions_["jpeg"] = "image/jpeg";
+  extensions_["jpg"]  = "image/jpeg";
+  extensions_["js"]   = "application/javascript";
+  extensions_["json"] = "application/json";
+  extensions_["nexe"] = "application/x-nacl";
+  extensions_["nmf"]  = "application/json";
+  extensions_["pexe"] = "application/x-pnacl";
+  extensions_["png"]  = "image/png";
+  extensions_["svg"]  = "image/svg+xml";
+  extensions_["wasm"] = "application/wasm";
+  extensions_["woff"] = "application/x-font-woff";
+  extensions_["xml"]  = "application/xml";
 }
 
 
-static bool ReadConfiguration(Json::Value& configuration,
-                              OrthancPluginContext* context)
+static std::string GetMimeType(const std::string& path)
 {
-  std::string s;
+  size_t dot = path.find_last_of('.');
+
+  std::string extension = (dot == std::string::npos) ? "" : path.substr(dot + 1);
+  std::transform(extension.begin(), extension.end(), extension.begin(), tolower);
 
+  std::map<std::string, std::string>::const_iterator found = extensions_.find(extension);
+
+  if (found != extensions_.end() &&
+      !found->second.empty())
   {
-    char* tmp = OrthancPluginGetConfiguration(context);
-    if (tmp == NULL)
-    {
-      OrthancPluginLogError(context, "Error while retrieving the configuration from Orthanc");
-      return false;
-    }
-
-    s.assign(tmp);
-    OrthancPluginFreeString(context, tmp);      
-  }
-
-  Json::Reader reader;
-  if (reader.parse(s, configuration))
-  {
-    return true;
+    return found->second;
   }
   else
   {
-    OrthancPluginLogError(context, "Unable to parse the configuration");
-    return false;
+    OrthancPlugins::LogWarning(context_, "ServeFolders: Unknown MIME type for extension \"" + extension + "\"");
+    return "application/octet-stream";
   }
 }
 
 
-
 static bool LookupFolder(std::string& folder,
                          OrthancPluginRestOutput* output,
                          const OrthancPluginHttpRequest* request)
@@ -143,8 +104,7 @@
   std::map<std::string, std::string>::const_iterator found = folders_.find(uri);
   if (found == folders_.end())
   {
-    std::string s = "Unknown URI in plugin server-folders: " + uri;
-    OrthancPluginLogError(context_, s.c_str());
+    OrthancPlugins::LogError(context_, "Unknown URI in plugin server-folders: " + uri);
     OrthancPluginSendHttpStatusCode(context_, output, 404);
     return false;
   }
@@ -156,16 +116,35 @@
 }
 
 
-static OrthancPluginErrorCode FolderCallback(OrthancPluginRestOutput* output,
-                                             const char* url,
-                                             const OrthancPluginHttpRequest* request)
+static void Answer(OrthancPluginRestOutput* output,
+                   const char* content,
+                   size_t size,
+                   const std::string& mime)
+{
+  if (generateETag_)
+  {
+    OrthancPlugins::OrthancString md5(context_);
+    md5.Assign(OrthancPluginComputeMd5(context_, content, size));
+
+    std::string etag = "\"" + std::string(md5.GetContent()) + "\"";
+    OrthancPluginSetHttpHeader(context_, output, "ETag", etag.c_str());
+  }
+
+  SetHttpHeaders(output);
+  OrthancPluginAnswerBuffer(context_, output, content, size, mime.c_str());
+}
+
+
+void ServeFolder(OrthancPluginRestOutput* output,
+                 const char* url,
+                 const OrthancPluginHttpRequest* request)
 {
   namespace fs = boost::filesystem;  
 
   if (request->method != OrthancPluginHttpMethod_Get)
   {
     OrthancPluginSendMethodNotAllowed(context_, output, "GET");
-    return OrthancPluginErrorCode_Success;
+    return;
   }
 
   std::string folder;
@@ -198,7 +177,10 @@
 
       for (fs::directory_iterator it(parent) ; it != end; ++it)
       {
-        if (fs::is_regular_file(it->status()))
+        fs::file_type type = it->status().type();
+
+        if (type == fs::regular_file ||
+            type == fs::reparse_file)  // cf. BitBucket issue #11
         {
           std::string f = it->path().filename().string();
           s += "      <li><a href=\"" + f + "\">" + f + "</a></li>\n";
@@ -209,40 +191,42 @@
       s += "  </body>\n";
       s += "</html>\n";
 
-      OrthancPluginAnswerBuffer(context_, output, s.c_str(), s.size(), "text/html");
+      Answer(output, s.c_str(), s.size(), "text/html");
     }
     else
     {
       std::string path = folder + "/" + item.string();
-      const char* mime = GetMimeType(path);
+      std::string mime = GetMimeType(path);
 
-      std::string s;
-      if (ReadFile(s, path))
+      OrthancPlugins::MemoryBuffer content(context_);
+
+      try
       {
-        const char* resource = s.size() ? s.c_str() : NULL;
-        OrthancPluginAnswerBuffer(context_, output, resource, s.size(), mime);
+        content.ReadFile(path);
       }
-      else
+      catch (...)
       {
-        std::string s = "Inexistent file in served folder: " + path;
-        OrthancPluginLogError(context_, s.c_str());
-        OrthancPluginSendHttpStatusCode(context_, output, 404);
+        ORTHANC_PLUGINS_THROW_EXCEPTION(InexistentFile);
       }
+
+      boost::posix_time::ptime lastModification = boost::posix_time::from_time_t(fs::last_write_time(path));
+      std::string t = boost::posix_time::to_iso_string(lastModification);
+      OrthancPluginSetHttpHeader(context_, output, "Last-Modified", t.c_str());
+
+      Answer(output, content.GetData(), content.GetSize(), mime);
     }
   }
-
-  return OrthancPluginErrorCode_Success;
 }
 
 
-static OrthancPluginErrorCode ListServedFolders(OrthancPluginRestOutput* output,
-                                                const char* url,
-                                                const OrthancPluginHttpRequest* request)
+void ListServedFolders(OrthancPluginRestOutput* output,
+                       const char* url,
+                       const OrthancPluginHttpRequest* request)
 {
   if (request->method != OrthancPluginHttpMethod_Get)
   {
     OrthancPluginSendMethodNotAllowed(context_, output, "GET");
-    return OrthancPluginErrorCode_Success;
+    return;
   }
 
   std::string s = "<html><body><h1>Additional folders served by Orthanc</h1>\n";
@@ -266,9 +250,161 @@
 
   s += "</body></html>\n";
 
-  OrthancPluginAnswerBuffer(context_, output, s.c_str(), s.size(), "text/html");
+  Answer(output, s.c_str(), s.size(), "text/html");
+}
+
+
+static void ConfigureFolders(const Json::Value& folders)
+{
+  if (folders.type() != Json::objectValue)
+  {
+    OrthancPlugins::LogError(context_, "The list of folders to be served is badly formatted (must be a JSON object)");
+    ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+  }
+
+  Json::Value::Members members = folders.getMemberNames();
+
+  // Register the callback for each base URI
+  for (Json::Value::Members::const_iterator 
+         it = members.begin(); it != members.end(); ++it)
+  {
+    if (folders[*it].type() != Json::stringValue)
+    {
+      OrthancPlugins::LogError(context_, "The folder to be served \"" + *it + 
+                               "\" must be associated with a string value (its mapped URI)");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+
+    std::string baseUri = *it;
+
+    // Remove the heading and trailing slashes in the root URI, if any
+    while (!baseUri.empty() &&
+           *baseUri.begin() == '/')
+    {
+      baseUri = baseUri.substr(1);
+    }
+
+    while (!baseUri.empty() &&
+           *baseUri.rbegin() == '/')
+    {
+      baseUri.resize(baseUri.size() - 1);
+    }
+
+    if (baseUri.empty())
+    {
+      OrthancPlugins::LogError(context_, "The URI of a folder to be served cannot be empty");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+
+    // Check whether the source folder exists and is indeed a directory
+    const std::string folder = folders[*it].asString();
+    if (!boost::filesystem::is_directory(folder))
+    {
+      OrthancPlugins::LogError(context_, "Trying and serve an inexistent folder: " + folder);
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InexistentFile);
+    }
+
+    folders_[baseUri] = folder;
+
+    // Register the callback to serve the folder
+    {
+      const std::string regex = "/(" + baseUri + ")/(.*)";
+      OrthancPlugins::RegisterRestCallback<ServeFolder>(context_, regex.c_str(), true);
+    }
+  }
+}
+
+
+static void ConfigureExtensions(const Json::Value& extensions)
+{
+  if (extensions.type() != Json::objectValue)
+  {
+    OrthancPlugins::LogError(context_, "The list of extensions is badly formatted (must be a JSON object)");
+    ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+  }
+
+  Json::Value::Members members = extensions.getMemberNames();
 
-  return OrthancPluginErrorCode_Success;
+  for (Json::Value::Members::const_iterator 
+         it = members.begin(); it != members.end(); ++it)
+  {
+    if (extensions[*it].type() != Json::stringValue)
+    {
+      OrthancPlugins::LogError(context_, "The file extension \"" + *it + 
+                               "\" must be associated with a string value (its MIME type)");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+
+    const std::string& mime = extensions[*it].asString();
+
+    std::string name = *it;
+
+    if (!name.empty() &&
+        name[0] == '.')
+    {
+      name = name.substr(1);  // Remove the leading dot "."
+    }
+
+    extensions_[name] = mime;
+
+    if (mime.empty())
+    {
+      OrthancPlugins::LogWarning(context_, "ServeFolders: Removing MIME type for file extension \"." + name + "\"");
+    }
+    else
+    {
+      OrthancPlugins::LogWarning(context_, "ServeFolders: Associating file extension \"." + name + 
+                                 "\" with MIME type \"" + mime + "\"");
+    }
+  }  
+}
+
+
+static void ReadConfiguration()
+{
+  OrthancPlugins::OrthancConfiguration configuration;
+
+  {
+    OrthancPlugins::OrthancConfiguration globalConfiguration(context_);
+    globalConfiguration.GetSection(configuration, "ServeFolders");
+  }
+
+  if (!configuration.IsSection("Folders"))
+  {
+    // This is a basic configuration
+    ConfigureFolders(configuration.GetJson());
+  }
+  else
+  {
+    // This is an advanced configuration
+    ConfigureFolders(configuration.GetJson()["Folders"]);
+
+    bool tmp;
+
+    if (configuration.LookupBooleanValue(tmp, "AllowCache"))
+    {
+      allowCache_ = tmp;
+      OrthancPlugins::LogWarning(context_, "ServeFolders: Requesting the HTTP client to " +
+                                 std::string(tmp ? "enable" : "disable") + 
+                                 " its caching mechanism");
+    }
+
+    if (configuration.LookupBooleanValue(tmp, "GenerateETag"))
+    {
+      generateETag_ = tmp;
+      OrthancPlugins::LogWarning(context_, "ServeFolders: The computation of an ETag for the served resources is " +
+                                 std::string(tmp ? "enabled" : "disabled"));
+    }
+
+    OrthancPlugins::OrthancConfiguration extensions;
+    configuration.GetSection(extensions, "Extensions");
+    ConfigureExtensions(extensions.GetJson());
+  }
+
+  if (folders_.empty())
+  {
+    OrthancPlugins::LogWarning(context_, "ServeFolders: Empty configuration file: No additional folder will be served!");
+  }
 }
 
 
@@ -281,87 +417,27 @@
     /* 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;
-    }
-
-    OrthancPluginSetDescription(context_, "Serve additional folders with the HTTP server of Orthanc.");
-
-    Json::Value configuration;
-    if (!ReadConfiguration(configuration, context_))
-    {
+      OrthancPlugins::ReportMinimalOrthancVersion(context_, 
+                                                  ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
       return -1;
     }
 
-    if (configuration.isMember("ServeFolders"))
-    {
-      if (configuration["ServeFolders"].type() != Json::objectValue)
-      {
-        OrthancPluginLogError(context_, "The \"ServeFolders\" configuration section is badly formatted (must be a JSON object)");
-        return -1;
-      }
-    }
-    else
-    {
-      OrthancPluginLogWarning(context_, "No section \"ServeFolders\" in your configuration file: "
-                              "No additional folder will be served!");
-      configuration["ServeFolders"] = Json::objectValue;
-    }
-
-
-    Json::Value::Members members = configuration["ServeFolders"].getMemberNames();
-
-    // Register the callback for each base URI
-    for (Json::Value::Members::const_iterator 
-           it = members.begin(); it != members.end(); ++it)
-    {
-      std::string baseUri = *it;
-
-      // Remove the heading and trailing slashes in the root URI, if any
-      while (!baseUri.empty() &&
-             *baseUri.begin() == '/')
-      {
-        baseUri = baseUri.substr(1);
-      }
+    RegisterDefaultExtensions();
+    OrthancPluginSetDescription(context_, "Serve additional folders with the HTTP server of Orthanc.");
+    OrthancPluginSetRootUri(context, INDEX_URI);
+    OrthancPlugins::RegisterRestCallback<ListServedFolders>(context_, INDEX_URI, true);
 
-      while (!baseUri.empty() &&
-             *baseUri.rbegin() == '/')
-      {
-        baseUri.resize(baseUri.size() - 1);
-      }
-
-      if (baseUri.empty())
-      {
-        OrthancPluginLogError(context_, "The URI of a folder to be served cannot be empty");
-        return -1;
-      }
-
-      // Check whether the source folder exists and is indeed a directory
-      const std::string folder = configuration["ServeFolders"][*it].asString();
-      if (!boost::filesystem::is_directory(folder))
-      {
-        std::string msg = "Trying and serve an inexistent folder: " + folder;
-        OrthancPluginLogError(context_, msg.c_str());
-        return -1;
-      }
-
-      folders_[baseUri] = folder;
-
-      // Register the callback to serve the folder
-      {
-        const std::string regex = "/(" + baseUri + ")/(.*)";
-        OrthancPluginRegisterRestCallback(context, regex.c_str(), FolderCallback);
-      }
+    try
+    {
+      ReadConfiguration();
     }
-
-    OrthancPluginRegisterRestCallback(context, INDEX_URI, ListServedFolders);
-    OrthancPluginSetRootUri(context, INDEX_URI);
+    catch (OrthancPlugins::PluginException& e)
+    {
+      OrthancPlugins::LogError(context_, "Error while initializing the ServeFolders plugin: " + 
+                               std::string(e.What(context_)));
+    }
 
     return 0;
   }
--- a/Plugins/Samples/ServeFolders/README	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Samples/ServeFolders/README	Fri Oct 12 15:18:10 2018 +0200
@@ -1,52 +1,11 @@
-Introduction
-============
+ServeFolders plugin
+===================
 
 This sample plugin enables Orthanc to serve additional folders using
 its embedded Web server.
 
-
-Compilation for Linux
-=====================
-
-# mkdir Build
-# cd Build
-# cmake ..
-# make
-
-
-Cross-compilation for Windows using MinGW
-=========================================
-
-# mkdir Build
-# cd Build
-# cmake .. -DCMAKE_TOOLCHAIN_FILE=../../../Resources/MinGWToolchain.cmake
-# make
-
-
-Configuration
-=============
+The shared library containing the plugin is created as part of the
+build process of Orthanc.
 
-First, generate the default configuration of Orthanc:
-https://code.google.com/p/orthanc/wiki/OrthancConfiguration
-
-Then, modify the "Plugins" option to point to the folder containing
-the built plugins.
-
-Finally, create a section "ServeFolders" in the configuration file to
-specify which folder you want to serve, and at which URI. For
-instance, the following excerpt would load the plugins from the
-working directory, then would branch the content of the folder
-"/home/jodogne/WWW/fosdem" as the URI "http://localhost:8042/fosdem":
-
-{
-  "Name" : "MyOrthanc",
-  [...]
-  "HttpPort" : 8042,
-  [...]
-  "Plugins" : [ 
-    "."
-  ],
-  "ServeFolders" : {
-    "/fosdem" : "/home/jodogne/WWW/fosdem"
-  }
-}
+Documentation is available in the Orthanc Book:
+http://book.orthanc-server.com/plugins/serve-folders.html
--- a/Plugins/Samples/StorageArea/Plugin.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Samples/StorageArea/Plugin.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Plugins/Samples/WebSkeleton/Configuration.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Samples/WebSkeleton/Configuration.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py	Fri Oct 12 15:18:10 2018 +0200
@@ -1,6 +1,7 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Plugins/Samples/WebSkeleton/Framework/Framework.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Samples/WebSkeleton/Framework/Framework.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,6 +1,7 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Plugins/Samples/WebSkeleton/Framework/Plugin.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Plugins/Samples/WebSkeleton/Framework/Plugin.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -65,6 +66,10 @@
   {
     return "application/xml";
   }
+  else if (extension == ".wasm")
+  {
+    return "application/wasm";
+  }
   else if (extension == ".png")
   {
     return "image/png";
--- a/README	Thu Oct 29 11:25:45 2015 +0100
+++ b/README	Fri Oct 12 15:18:10 2018 +0200
@@ -16,11 +16,14 @@
 Supported Platforms
 -------------------
 
-Currently, the supported platforms are:
+Currently, the officially validated platforms are:
 
-* Linux 32bit.
-* Linux 64bit.
-* Windows 32bit.
+* GNU/Linux (32bit and 64bit).
+* Windows (32bit and 64bit).
+* Apple OS X (32bit and 64bit).
+
+Orthanc is known to work on other UNIX-like platforms (such as FreeBSD
+and OpenBSD).
 
 
 Supported Toolchains
@@ -29,9 +32,9 @@
 Orthanc can currently be built using the following compiling
 toolchains:
 
-* Native Linux compilation, with gcc.
+* Native GNU/Linux compilation, with gcc.
 * Native Windows compilation, with Microsoft Visual Studio.
-* Cross-compilation for Windows under Linux, with MinGW.
+* Cross-compilation for Windows under GNU/Linux, with MinGW.
 
 
 Licensing
@@ -47,16 +50,19 @@
 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}}
+@Article{Jodogne2018,
+  author="Jodogne, S{\'e}bastien",
+  title="The {O}rthanc Ecosystem for Medical Imaging",
+  journal="Journal of Digital Imaging",
+  year="2018",
+  month="Jun",
+  day="01",
+  volume="31",
+  number="3",
+  pages="341--352",
+  issn="1618-727X",
+  doi="10.1007/s10278-018-0082-y",
+  url="https://doi.org/10.1007/s10278-018-0082-y"
 }
 
 
--- a/Resources/CMake/BoostConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/BoostConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -7,10 +7,26 @@
   #set(Boost_DEBUG 1)
   #set(Boost_USE_STATIC_LIBS ON)
 
-  find_package(Boost
-    COMPONENTS filesystem thread system date_time regex locale)
+  if (ENABLE_LOCALE)
+    list(APPEND ORTHANC_BOOST_COMPONENTS locale)
+  endif()
+
+  list(APPEND ORTHANC_BOOST_COMPONENTS filesystem thread system date_time regex)
+  find_package(Boost COMPONENTS "${ORTHANC_BOOST_COMPONENTS}")
 
   if (NOT Boost_FOUND)
+    foreach (item ${ORTHANC_BOOST_COMPONENTS})
+      string(TOUPPER ${item} tmp)
+
+      if (Boost_${tmp}_FOUND)
+        set(tmp2 "found")
+      else()
+        set(tmp2 "missing")
+      endif()
+      
+      message("Boost component ${item} - ${tmp2}")
+    endforeach()
+    
     message(FATAL_ERROR "Unable to locate Boost on this system")
   endif()
 
@@ -27,86 +43,58 @@
       )
   endif()
 
-  #if (${Boost_VERSION} LESS 104800)
-  # boost::locale is only available from 1.48.00
-  #message("Too old version of Boost (${Boost_LIB_VERSION}): Building the static version")
-  #  set(BOOST_STATIC 1)
-  #endif()
-
   include_directories(${Boost_INCLUDE_DIRS})
   link_libraries(${Boost_LIBRARIES})
 endif()
 
 
 if (BOOST_STATIC)
-  # Parameters for Boost 1.58.0
-  set(BOOST_NAME boost_1_58_0)
-  set(BOOST_BCP_SUFFIX bcpdigest-0.9.2)
-  set(BOOST_MD5 "704b110917cbda903e07cb53934b47ac")
-  set(BOOST_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz")
-  set(BOOST_FILESYSTEM_SOURCES_DIR "${BOOST_NAME}/libs/filesystem/src") 
+  ##
+  ## Parameters for static compilation of Boost 
+  ##
+  
+  set(BOOST_NAME boost_1_67_0)
+  set(BOOST_VERSION 1.67.0)
+  set(BOOST_BCP_SUFFIX bcpdigest-1.4.0)
+  set(BOOST_MD5 "fb3535a88e72c3d4c4d06b047b8e57fe")
+  set(BOOST_URL "http://www.orthanc-server.com/downloads/third-party/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz")
   set(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME})
 
+  if (IS_DIRECTORY "${BOOST_SOURCES_DIR}")
+    set(FirstRun OFF)
+  else()
+    set(FirstRun ON)
+  endif()
+
   DownloadPackage(${BOOST_MD5} ${BOOST_URL} "${BOOST_SOURCES_DIR}")
 
-  set(BOOST_SOURCES)
 
-  if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
-    list(APPEND BOOST_SOURCES
-      ${BOOST_SOURCES_DIR}/libs/thread/src/pthread/once.cpp
-      ${BOOST_SOURCES_DIR}/libs/thread/src/pthread/thread.cpp
-      )
-    add_definitions(
-      -DBOOST_LOCALE_WITH_ICONV=1
-      )
-
-    if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-      add_definitions(-DBOOST_HAS_SCHED_YIELD=1)
-    endif()
+  ##
+  ## Patching boost
+  ## 
 
-  elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-    list(APPEND BOOST_SOURCES
-      ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_dll.cpp
-      ${BOOST_SOURCES_DIR}/libs/thread/src/win32/thread.cpp
-      ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_pe.cpp
-      ${BOOST_FILESYSTEM_SOURCES_DIR}/windows_file_codecvt.cpp
-      )
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+    ${ORTHANC_ROOT}/Resources/Patches/boost-${BOOST_VERSION}-linux-standard-base.patch
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    RESULT_VARIABLE Failure
+    )
 
-    # Starting with release 0.8.2, Orthanc statically links against
-    # libiconv, even on Windows. Indeed, the "WCONV" library of
-    # Windows XP seems not to support properly several codepages
-    # (notably "Latin3", "Hebrew", and "Arabic").
-
-    if (USE_BOOST_ICONV)
-      include(${ORTHANC_ROOT}/Resources/CMake/LibIconvConfiguration.cmake)
-    else()
-      add_definitions(-DBOOST_LOCALE_WITH_WCONV=1)
-    endif()
-
-  else()
-    message(FATAL_ERROR "Support your platform here")
+  if (FirstRun AND Failure)
+    message(FATAL_ERROR "Error while patching a file")
   endif()
 
-  if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
-    list(APPEND BOOST_SOURCES
-      ${BOOST_SOURCES_DIR}/libs/filesystem/src/utf8_codecvt_facet.cpp
-      )
+
+  ##
+  ## Generic configuration of Boost
+  ## 
+
+  if (CMAKE_COMPILER_IS_GNUCXX)
+    add_definitions(-isystem ${BOOST_SOURCES_DIR})
   endif()
 
-  aux_source_directory(${BOOST_SOURCES_DIR}/libs/regex/src BOOST_REGEX_SOURCES)
-
-  list(APPEND BOOST_SOURCES
-    ${BOOST_REGEX_SOURCES}
-    ${BOOST_SOURCES_DIR}/libs/date_time/src/gregorian/greg_month.cpp
-    ${BOOST_FILESYSTEM_SOURCES_DIR}/codecvt_error_category.cpp
-    ${BOOST_FILESYSTEM_SOURCES_DIR}/operations.cpp
-    ${BOOST_FILESYSTEM_SOURCES_DIR}/path.cpp
-    ${BOOST_FILESYSTEM_SOURCES_DIR}/path_traits.cpp
-    ${BOOST_SOURCES_DIR}/libs/locale/src/encoding/codepage.cpp
-    ${BOOST_SOURCES_DIR}/libs/system/src/error_code.cpp
+  include_directories(
+    ${BOOST_SOURCES_DIR}
     )
 
   add_definitions(
@@ -119,27 +107,213 @@
     -DBOOST_REGEX_NO_LIB
     -DBOOST_SYSTEM_NO_LIB
     -DBOOST_LOCALE_NO_LIB
-    -DBOOST_HAS_LOCALE=1
-    -DBOOST_HAS_FILESYSTEM_V3=1
+    )
+
+  set(BOOST_SOURCES
+    ${BOOST_SOURCES_DIR}/libs/system/src/error_code.cpp
     )
 
-  if (CMAKE_COMPILER_IS_GNUCXX)
-    add_definitions(-isystem ${BOOST_SOURCES_DIR})
+  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase" OR
+      "${CMAKE_SYSTEM_NAME}" STREQUAL "Android")
+    add_definitions(
+      -DBOOST_SYSTEM_USE_STRERROR=1
+      )
   endif()
 
-  include_directories(
-    ${BOOST_SOURCES_DIR}
+  
+  ##
+  ## Configuration of boost::thread
+  ##
+  
+  if (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR
+      CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR
+      CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR
+      CMAKE_SYSTEM_NAME STREQUAL "kFreeBSD" OR
+      CMAKE_SYSTEM_NAME STREQUAL "OpenBSD" OR
+      CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR
+      CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR
+      CMAKE_SYSTEM_NAME STREQUAL "NaCl64" OR
+      CMAKE_SYSTEM_NAME STREQUAL "Android")
+    list(APPEND BOOST_SOURCES
+      ${BOOST_SOURCES_DIR}/libs/atomic/src/lockpool.cpp
+      ${BOOST_SOURCES_DIR}/libs/thread/src/pthread/once.cpp
+      ${BOOST_SOURCES_DIR}/libs/thread/src/pthread/thread.cpp
+      )
+
+    if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase" OR
+        CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR
+        CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR
+        CMAKE_SYSTEM_NAME STREQUAL "NaCl64")
+      add_definitions(-DBOOST_HAS_SCHED_YIELD=1)
+    endif()
+
+  elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
+    list(APPEND BOOST_SOURCES
+      ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_dll.cpp
+      ${BOOST_SOURCES_DIR}/libs/thread/src/win32/thread.cpp
+      ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_pe.cpp
+      )
+
+  elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+    # No support for threads in asm.js/WebAssembly
+
+  else()
+    message(FATAL_ERROR "Support your platform here")
+  endif()
+
+
+  ##
+  ## Configuration of boost::regex
+  ##
+  
+  aux_source_directory(${BOOST_SOURCES_DIR}/libs/regex/src BOOST_REGEX_SOURCES)
+
+  list(APPEND BOOST_SOURCES
+    ${BOOST_REGEX_SOURCES}
+    )
+
+
+  ##
+  ## Configuration of boost::datetime
+  ##
+  
+  list(APPEND BOOST_SOURCES
+    ${BOOST_SOURCES_DIR}/libs/date_time/src/gregorian/greg_month.cpp
     )
 
-  source_group(ThirdParty\\Boost REGULAR_EXPRESSION ${BOOST_SOURCES_DIR}/.*)
-else()
-  add_definitions(
-    -DBOOST_HAS_LOCALE=1
-    )
-endif()
+
+  ##
+  ## Configuration of boost::filesystem
+  ## 
+
+  if (CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR
+      CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR
+      CMAKE_SYSTEM_NAME STREQUAL "NaCl64" OR
+      CMAKE_SYSTEM_NAME STREQUAL "Android")
+    # boost::filesystem is not available on PNaCl
+    add_definitions(
+      -DBOOST_HAS_FILESYSTEM_V3=0
+      -D__INTEGRITY=1
+      )
+  else()
+    add_definitions(
+      -DBOOST_HAS_FILESYSTEM_V3=1
+      )
+    list(APPEND BOOST_SOURCES
+      ${BOOST_NAME}/libs/filesystem/src/codecvt_error_category.cpp
+      ${BOOST_NAME}/libs/filesystem/src/operations.cpp
+      ${BOOST_NAME}/libs/filesystem/src/path.cpp
+      ${BOOST_NAME}/libs/filesystem/src/path_traits.cpp
+      )
+
+    if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR
+        CMAKE_SYSTEM_NAME STREQUAL "OpenBSD" OR
+        CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
+     list(APPEND BOOST_SOURCES
+        ${BOOST_SOURCES_DIR}/libs/filesystem/src/utf8_codecvt_facet.cpp
+        )
+
+    elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows")
+      list(APPEND BOOST_SOURCES
+        ${BOOST_NAME}/libs/filesystem/src/windows_file_codecvt.cpp
+        )
+    endif()
+  endif()
 
 
-add_definitions(
-  -DBOOST_HAS_DATE_TIME=1
-  -DBOOST_HAS_REGEX=1
-  )
+  ##
+  ## Configuration of boost::locale
+  ## 
+
+  if (NOT ENABLE_LOCALE)
+    message("boost::locale is disabled")
+  else()
+    list(APPEND BOOST_SOURCES
+      ${BOOST_SOURCES_DIR}/libs/locale/src/encoding/codepage.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/generator.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/date_time.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/formatting.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/ids.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/localization_backend.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/message.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/mo_lambda.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/util/codecvt_converter.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/util/default_locale.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/util/gregorian.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/util/info.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/util/locale_data.cpp
+      )        
+
+    if (CMAKE_SYSTEM_NAME STREQUAL "OpenBSD" OR
+        CMAKE_SYSTEM_VERSION STREQUAL "LinuxStandardBase")
+      list(APPEND BOOST_SOURCES
+        ${BOOST_SOURCES_DIR}/libs/locale/src/std/codecvt.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/std/collate.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/std/converter.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/std/numeric.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/std/std_backend.cpp
+        )
+
+      add_definitions(
+        -DBOOST_LOCALE_WITH_ICONV=1
+        -DBOOST_LOCALE_NO_WINAPI_BACKEND=1
+        -DBOOST_LOCALE_NO_POSIX_BACKEND=1
+        )
+      
+    elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR
+            CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR
+            CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR
+            CMAKE_SYSTEM_NAME STREQUAL "kFreeBSD" OR
+            CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR
+            CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR
+            CMAKE_SYSTEM_NAME STREQUAL "NaCl64" OR
+            CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # For WebAssembly or asm.js
+      list(APPEND BOOST_SOURCES
+        ${BOOST_SOURCES_DIR}/libs/locale/src/posix/codecvt.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/posix/collate.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/posix/converter.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/posix/numeric.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/posix/posix_backend.cpp
+        )
+
+      add_definitions(
+        -DBOOST_LOCALE_WITH_ICONV=1
+        -DBOOST_LOCALE_NO_WINAPI_BACKEND=1
+        -DBOOST_LOCALE_NO_STD_BACKEND=1
+        )
+      
+    elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows")
+      list(APPEND BOOST_SOURCES
+        ${BOOST_SOURCES_DIR}/libs/locale/src/win32/collate.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/win32/converter.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/win32/lcid.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/win32/numeric.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/win32/win_backend.cpp
+        )
+
+      add_definitions(
+        -DBOOST_LOCALE_NO_POSIX_BACKEND=1
+        -DBOOST_LOCALE_NO_STD_BACKEND=1
+        )
+
+      # Starting with release 0.8.2, Orthanc statically links against
+      # libiconv, even on Windows. Indeed, the "WCONV" library of
+      # Windows XP seems not to support properly several codepages
+      # (notably "Latin3", "Hebrew", and "Arabic"). Set
+      # "USE_BOOST_ICONV" to "OFF" to use WCONV anyway.
+
+      if (USE_BOOST_ICONV)
+        add_definitions(-DBOOST_LOCALE_WITH_ICONV=1)
+      else()
+        add_definitions(-DBOOST_LOCALE_WITH_WCONV=1)
+      endif()
+
+    else()
+      message(FATAL_ERROR "Support your platform here")
+    endif()
+  endif()
+
+  
+  source_group(ThirdParty\\boost REGULAR_EXPRESSION ${BOOST_SOURCES_DIR}/.*)
+
+endif()
--- a/Resources/CMake/BoostConfiguration.sh	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/BoostConfiguration.sh	Fri Oct 12 15:18:10 2018 +0200
@@ -13,22 +13,53 @@
 ## History:
 ##   - Orthanc between 0.6.2 and 0.7.3: Boost 1.54.0
 ##   - Orthanc between 0.7.4 and 0.9.1: Boost 1.55.0
-##   - Orthanc >= 0.9.2: Boost 1.58.0
+##   - Orthanc between 0.9.2 and 0.9.4: Boost 1.58.0
+##   - Orthanc between 0.9.5 and 1.0.0: Boost 1.59.0
+##   - Orthanc between 1.1.0 and 1.2.0: Boost 1.60.0
+##   - Orthanc 1.3.0: Boost 1.64.0
+##   - Orthanc 1.3.1: Boost 1.65.1
+##   - Orthanc 1.3.2: Boost 1.66.0
+##   - Orthanc >= 1.4.0: Boost 1.67.0
 
-rm -rf /tmp/boost_1_58_0
-rm -rf /tmp/bcp/boost_1_58_0
+BOOST_VERSION=1_67_0
+ORTHANC_VERSION=1.4.0
+
+rm -rf /tmp/boost_${BOOST_VERSION}
+rm -rf /tmp/bcp/boost_${BOOST_VERSION}
 
 cd /tmp
-echo "Uncompressing the sources of Boost 1.58.0..."
-tar xfz ./boost_1_58_0.tar.gz 
+echo "Uncompressing the sources of Boost ${BOOST_VERSION}..."
+tar xfz ./boost_${BOOST_VERSION}.tar.gz 
 
 echo "Generating the subset..."
-mkdir -p /tmp/bcp/boost_1_58_0
-bcp --boost=/tmp/boost_1_58_0 thread system locale date_time filesystem math/special_functions algorithm uuid atomic /tmp/bcp/boost_1_58_0
-cd /tmp/bcp
+mkdir -p /tmp/bcp/boost_${BOOST_VERSION}
+bcp --boost=/tmp/boost_${BOOST_VERSION} thread system locale date_time filesystem math/special_functions algorithm uuid atomic iostreams program_options numeric/ublas geometry polygon signals2 /tmp/bcp/boost_${BOOST_VERSION}
+
+echo "Removing documentation..."
+rm -rf /tmp/bcp/boost_${BOOST_VERSION}/libs/locale/doc/html
+rm -rf /tmp/bcp/boost_${BOOST_VERSION}/libs/algorithm/doc/html
+rm -rf /tmp/bcp/boost_${BOOST_VERSION}/libs/geometry/doc/html
+rm -rf /tmp/bcp/boost_${BOOST_VERSION}/libs/geometry/doc/doxy/doxygen_output/html
+rm -rf /tmp/bcp/boost_${BOOST_VERSION}/libs/filesystem/example/
+
+# https://stackoverflow.com/questions/1655372/longest-line-in-a-file
+LONGEST_FILENAME=`find /tmp/bcp/ | awk '{print length, $0}' | sort -nr | head -1`
+LONGEST=`echo "$LONGEST_FILENAME" | cut -d ' ' -f 1`
+
+echo
+echo "Longest filename (${LONGEST} characters):"
+echo "${LONGEST_FILENAME}"
+echo
+
+if [ ${LONGEST} -ge 128 ]; then
+    echo "ERROR: Too long filename for Windows!"
+    echo
+    exit -1
+fi
 
 echo "Compressing the subset..."
-tar cfz boost_1_58_0_bcpdigest-0.9.2.tar.gz boost_1_58_0
-ls -l boost_1_58_0_bcpdigest-0.9.2.tar.gz
-md5sum boost_1_58_0_bcpdigest-0.9.2.tar.gz
-readlink -f boost_1_58_0_bcpdigest-0.9.2.tar.gz
+cd /tmp/bcp
+tar cfz boost_${BOOST_VERSION}_bcpdigest-${ORTHANC_VERSION}.tar.gz boost_${BOOST_VERSION}
+ls -l boost_${BOOST_VERSION}_bcpdigest-${ORTHANC_VERSION}.tar.gz
+md5sum boost_${BOOST_VERSION}_bcpdigest-${ORTHANC_VERSION}.tar.gz
+readlink -f boost_${BOOST_VERSION}_bcpdigest-${ORTHANC_VERSION}.tar.gz
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/CivetwebConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,55 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_CIVETWEB)
+  set(CIVETWEB_SOURCES_DIR ${CMAKE_BINARY_DIR}/civetweb-1.9.1)
+  set(CIVETWEB_URL "http://www.orthanc-server.com/downloads/third-party/civetweb-1.9.1.tar.gz")
+  set(CIVETWEB_MD5 "c713f7336582d1a78897971260c67c2a")
+
+  DownloadPackage(${CIVETWEB_MD5} ${CIVETWEB_URL} "${CIVETWEB_SOURCES_DIR}")
+
+  include_directories(
+    ${CIVETWEB_SOURCES_DIR}/include
+    )
+
+  set(CIVETWEB_SOURCES
+    ${CIVETWEB_SOURCES_DIR}/src/civetweb.c
+    )
+
+
+  if (ENABLE_SSL)
+    add_definitions(
+      -DNO_SSL_DL=1
+      )
+    if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
+        ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
+      link_libraries(dl)
+    endif()
+
+  else()
+    add_definitions(
+      -DNO_SSL=1   # Remove SSL support from civetweb
+      )
+  endif()
+
+
+  if (CMAKE_SYSTEM_NAME STREQUAL "Windows" AND
+      CMAKE_COMPILER_IS_GNUCXX)
+    # This is a patch for MinGW64
+    add_definitions(-D_TIMESPEC_DEFINED=1)
+  endif()
+
+  source_group(ThirdParty\\Civetweb REGULAR_EXPRESSION ${CIVETWEB_SOURCES_DIR}/.*)
+
+else()
+  CHECK_INCLUDE_FILE_CXX(civetweb.h HAVE_CIVETWEB_H)
+  if (NOT HAVE_CIVETWEB_H)
+    message(FATAL_ERROR "Please install the libcivetweb-devel package")
+  endif()
+
+  cmake_reset_check_state()
+  set(CMAKE_REQUIRED_LIBRARIES dl pthread)
+  CHECK_LIBRARY_EXISTS(civetweb mg_start "" HAVE_CIVETWEB_LIB)
+  if (NOT HAVE_CIVETWEB_LIB)
+    message(FATAL_ERROR "Please install the libcivetweb-devel package")
+  endif()
+
+  link_libraries(civetweb)
+endif()
--- a/Resources/CMake/Compiler.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/Compiler.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,16 +1,18 @@
 # This file sets all the compiler-related flags
 
-if (CMAKE_CROSSCOMPILING)
+if (CMAKE_CROSSCOMPILING OR
+    "${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
   # Cross-compilation necessarily implies standalone and static build
   SET(STATIC_BUILD ON)
   SET(STANDALONE_BUILD ON)
 endif()
 
 if (CMAKE_COMPILER_IS_GNUCXX)
-  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wno-long-long -Wno-implicit-function-declaration")  
+  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wno-long-long")
+
   # --std=c99 makes libcurl not to compile
   # -pedantic gives a lot of warnings on OpenSSL 
-  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic -Wno-long-long -Wno-variadic-macros")
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-long-long -Wno-variadic-macros")
 
   if (CMAKE_CROSSCOMPILING)
     # http://stackoverflow.com/a/3543845/881731
@@ -41,31 +43,80 @@
     -D_CRT_SECURE_NO_WARNINGS=1
     -D_CRT_SECURE_NO_DEPRECATE=1
     )
-  include_directories(${ORTHANC_ROOT}/Resources/ThirdParty/VisualStudio)
+
+  if (MSVC_VERSION LESS 1600)
+    # Starting with Visual Studio >= 2010 (i.e. macro _MSC_VER >=
+    # 1600), Microsoft ships a standard-compliant <stdint.h>
+    # header. For earlier versions of Visual Studio, give access to a
+    # compatibility header.
+    # http://stackoverflow.com/a/70630/881731
+    # https://en.wikibooks.org/wiki/C_Programming/C_Reference/stdint.h#External_links
+    include_directories(${ORTHANC_ROOT}/Resources/ThirdParty/VisualStudio)
+  endif()
+
   link_libraries(netapi32)
 endif()
 
 
+if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
+    ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+  # In FreeBSD/OpenBSD, the "/usr/local/" folder contains the ports and need to be imported
+  SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I/usr/local/include")
+  SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I/usr/local/include")
+  SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L/usr/local/lib")
+  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -L/usr/local/lib")
+endif()
+
+
 if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
     ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR
-    ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
-  set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined")
-  set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined -Wl,--version-script=${ORTHANC_ROOT}/Plugins/Samples/Common/VersionScript.map")
+    ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
+    ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+
+  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+    # The "--no-undefined" linker flag makes the shared libraries
+    # (plugins ModalityWorklists and ServeFolders) fail to compile on OpenBSD
+    set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined")
+    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined")
+  endif()
+
+  if (NOT DEFINED ENABLE_PLUGINS_VERSION_SCRIPT OR 
+      ENABLE_PLUGINS_VERSION_SCRIPT)
+    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--version-script=${ORTHANC_ROOT}/Plugins/Samples/Common/VersionScript.map")
+  endif()
 
   # Remove the "-rdynamic" option
   # http://www.mail-archive.com/cmake@cmake.org/msg08837.html
   set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "")
-  link_libraries(uuid pthread rt)
+  link_libraries(pthread)
+
+  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+    link_libraries(rt)
+  endif()
 
-  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
+  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND
+      NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+    link_libraries(dl)
+  endif()
+
+  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND
+      NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+    # The "--as-needed" linker flag is not available on FreeBSD and OpenBSD
     set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--as-needed")
     set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--as-needed")
     set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--as-needed")
+  endif()
+
+  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND
+      NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+    # FreeBSD/OpenBSD have just one single interface for file
+    # handling, which is 64bit clean, so there is no need to define macro
+    # for LFS (Large File Support).
+    # https://ohse.de/uwe/articles/lfs.html
     add_definitions(
       -D_LARGEFILE64_SOURCE=1 
       -D_FILE_OFFSET_BITS=64
       )
-    link_libraries(dl)
   endif()
 
 elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
@@ -92,6 +143,11 @@
   link_libraries(rpcrt4 ws2_32)
 
   if (CMAKE_COMPILER_IS_GNUCXX)
+    # Some additional C/C++ compiler flags for MinGW
+    SET(MINGW_NO_WARNINGS "-Wno-unused-function -Wno-unused-variable")
+    SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${MINGW_NO_WARNINGS} -Wno-pointer-to-int-cast -Wno-int-to-pointer-cast")
+    SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${MINGW_NO_WARNINGS}")
+
     # This is a patch for MinGW64
     SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--allow-multiple-definition -static-libgcc -static-libstdc++")
     SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--allow-multiple-definition -static-libgcc -static-libstdc++")
@@ -115,33 +171,48 @@
     )
   link_libraries(iconv)
 
-endif()
-
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  message("Building using Emscripten (for WebAssembly or asm.js targets)")
 
-if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-  SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -I${LSB_PATH}/include")
-  SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -nostdinc++ -I${LSB_PATH}/include -I${LSB_PATH}/include/c++ -I${LSB_PATH}/include/c++/backward -fpermissive")
-  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH}")
+  # The BINARYEN_TRAP_MODE specifies what to do when divisions per
+  # zero (and similar conditions like integer overflows) are
+  # encountered: The "clamp" mode avoids throwing errors, as they
+  # cannot be properly catched by "try {} catch (...)" constructions.
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]' -s BINARYEN_TRAP_MODE='\"clamp\"'")
+
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Android")
+
+else()
+  message("Unknown target platform: ${CMAKE_SYSTEM_NAME}")
+  message(FATAL_ERROR "Support your platform here")
 endif()
 
 
-if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
-  # In FreeBSD, the "/usr/local/" folder contains the ports and need to be imported
-  SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I/usr/local/include")
-  SET(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -I/usr/local/include")
-  SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L/usr/local/lib")
-  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -L/usr/local/lib")
+if (DEFINED ENABLE_PROFILING AND ENABLE_PROFILING)
+  if (CMAKE_COMPILER_IS_GNUCXX)
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg")
+    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pg")
+    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pg")
+    set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -pg")
+    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pg")
+  else()
+    message(FATAL_ERROR "Don't know how to enable profiling on your configuration")
+  endif()
 endif()
 
 
-if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-  CHECK_INCLUDE_FILES(rpc.h HAVE_UUID_H)
-else()
-  CHECK_INCLUDE_FILES(uuid/uuid.h HAVE_UUID_H)
-endif()
-
-if (NOT HAVE_UUID_H)
-  message(FATAL_ERROR "Please install the uuid-dev package")
+if (CMAKE_COMPILER_IS_GNUCXX)
+  # "When creating a static library using binutils (ar) and there
+  # exist a duplicate object name (e.g. a/Foo.cpp.o, b/Foo.cpp.o), the
+  # resulting static library can end up having only one of the
+  # duplicate objects. [...] This bug only happens if there are many
+  # objects." The trick consists in replacing the "r" argument
+  # ("replace") provided to "ar" (as used in CMake < 3.1) by the "q"
+  # argument ("quick append"). This is because of the fact that CMake
+  # will invoke "ar" several times with several batches of ".o"
+  # objects, and using "r" would overwrite symbols defined in
+  # preceding batches. https://cmake.org/Bug/view.php?id=14874
+  set(CMAKE_CXX_ARCHIVE_APPEND "<CMAKE_AR> <LINK_FLAGS> q <TARGET> <OBJECTS>")
 endif()
 
 
--- a/Resources/CMake/DcmtkConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/DcmtkConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,25 +1,34 @@
-# Lookup for DICOM dictionaries, if none is specified by the user
-if (DCMTK_DICTIONARY_DIR STREQUAL "")
-  find_path(DCMTK_DICTIONARY_DIR_AUTO dicom.dic
-    /usr/share/dcmtk
-    /usr/share/libdcmtk2
-    /usr/local/share/dcmtk
-    )
-
-  message("Autodetected path to the DICOM dictionaries: ${DCMTK_DICTIONARY_DIR_AUTO}")
-  add_definitions(-DDCMTK_DICTIONARY_DIR="${DCMTK_DICTIONARY_DIR_AUTO}")
-else()
-  add_definitions(-DDCMTK_DICTIONARY_DIR="${DCMTK_DICTIONARY_DIR}")
+if (NOT DEFINED ENABLE_DCMTK_NETWORKING)
+    set(ENABLE_DCMTK_NETWORKING ON)
 endif()
 
+if (STATIC_BUILD OR NOT USE_SYSTEM_DCMTK)
+  if (USE_DCMTK_360)
+    SET(DCMTK_VERSION_NUMBER 360)
+    SET(DCMTK_PACKAGE_VERSION "3.6.0")
+    SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.0)
+    SET(DCMTK_URL "http://www.orthanc-server.com/downloads/third-party/dcmtk-3.6.0.zip")
+    SET(DCMTK_MD5 "219ad631b82031806147e4abbfba4fa4")
+  else()
+    SET(DCMTK_VERSION_NUMBER 362)
+    SET(DCMTK_PACKAGE_VERSION "3.6.2")
+    SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.2)
+    SET(DCMTK_URL "http://www.orthanc-server.com/downloads/third-party/dcmtk-3.6.2.tar.gz")
+    SET(DCMTK_MD5 "d219a4152772985191c9b89d75302d12")
 
-if (STATIC_BUILD OR NOT USE_SYSTEM_DCMTK)
-  SET(DCMTK_VERSION_NUMBER 360)
-  set(DCMTK_PACKAGE_VERSION "3.6.0")
-  SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.0)
-  SET(DCMTK_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/dcmtk-3.6.0.zip")
-  SET(DCMTK_MD5 "219ad631b82031806147e4abbfba4fa4")
+    macro(DCMTK_UNSET)
+    endmacro()
+
+    macro(DCMTK_UNSET_CACHE)
+    endmacro()
 
+    set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/)
+    set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/)
+    set(DCMTK_WITH_THREADS ON)
+    
+    add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1)
+  endif()
+  
   if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}")
     set(FirstRun OFF)
   else()
@@ -28,26 +37,141 @@
 
   DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}")
 
+  
+  if (FirstRun)
+    if (USE_DCMTK_360)
+      # If using DCMTK 3.6.0, backport the "private.dic" file from DCMTK
+      # 3.6.2. This adds support for more private tags, and fixes some
+      # import problems with Philips MRI Achieva.
+      if (USE_DCMTK_362_PRIVATE_DIC)
+        message("Using the dictionary of private tags from DCMTK 3.6.2")
+        configure_file(
+          ${ORTHANC_ROOT}/Resources/Patches/dcmtk-3.6.2-private.dic
+          ${DCMTK_SOURCES_DIR}/dcmdata/data/private.dic
+          COPYONLY)
+      else()
+        message("Using the dictionary of private tags from DCMTK 3.6.0")
+      endif()
+      
+      # Patches specific to DCMTK 3.6.0
+      message("Applying patch to solve vulnerability in DCMTK 3.6.0")
+      execute_process(
+        COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+        ${ORTHANC_ROOT}/Resources/Patches/dcmtk-3.6.0-dulparse-vulnerability.patch
+        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+        RESULT_VARIABLE Failure
+        )
+
+      if (Failure)
+        message(FATAL_ERROR "Error while patching a file")
+      endif()
+
+      # This patch is not needed anymore thanks to the following commit
+      # (information sent by Jorg Riesmeier on Twitter on 2017-07-19):
+      # http://git.dcmtk.org/?p=dcmtk.git;a=commit;h=8df1f5e517b8629ae09088d0935c2a8dd333c76f
+      message("Applying patch for speed in DCMTK 3.6.0")
+      execute_process(
+        COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+        ${ORTHANC_ROOT}/Resources/Patches/dcmtk-3.6.0-speed.patch
+        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+        RESULT_VARIABLE Failure
+        )
+
+      if (Failure)
+        message(FATAL_ERROR "Error while patching a file")
+      endif()
+
+    else()
+      # "3.6.2 CXX11 fails on Linux; patch suggestions included"
+      # https://forum.dcmtk.org/viewtopic.php?f=3&t=4637
+      message("Applying patch to detect mathematic primitives in DCMTK 3.6.2 with C++11")
+      execute_process(
+        COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+        ${ORTHANC_ROOT}/Resources/Patches/dcmtk-3.6.2-cmath.patch
+        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+        RESULT_VARIABLE Failure
+        )
+
+      if (Failure)
+        message(FATAL_ERROR "Error while patching a file")
+      endif()
+    endif()
+  else()
+    message("The patches for DCMTK have already been applied")
+  endif()
+
+
+  # C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake"
   IF (CMAKE_CROSSCOMPILING)
-    SET(C_CHAR_UNSIGNED 1 CACHE INTERNAL "Whether char is unsigned.")
+    if (CMAKE_COMPILER_IS_GNUCXX AND
+        CMAKE_SYSTEM_NAME STREQUAL "Windows")  # MinGW
+      SET(C_CHAR_UNSIGNED 1 CACHE INTERNAL "Whether char is unsigned.")
+
+    elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")  # WebAssembly or asm.js
+
+      # Check out "../WebAssembly/ArithmeticTests/" to regenerate the
+      # "arith.h" file
+      configure_file(
+        ${ORTHANC_ROOT}/Resources/WebAssembly/arith.h
+        ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/arith.h
+        COPYONLY)
+
+      UNSET(C_CHAR_UNSIGNED CACHE)
+      SET(C_CHAR_UNSIGNED 0 CACHE INTERNAL "")
+
+    else()
+      message(FATAL_ERROR "Support your platform here")
+    endif()
   ENDIF()
+
+  
+  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
+    SET(DCMTK_ENABLE_CHARSET_CONVERSION "iconv" CACHE STRING "")
+    SET(HAVE_SYS_GETTID 0 CACHE INTERNAL "")
+
+    execute_process(
+      COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+      ${ORTHANC_ROOT}/Resources/Patches/dcmtk-3.6.2-linux-standard-base.patch
+      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+      RESULT_VARIABLE Failure
+      )
+
+    if (FirstRun AND Failure)
+      message(FATAL_ERROR "Error while patching a file")
+    endif()
+  endif()
+
   SET(DCMTK_SOURCE_DIR ${DCMTK_SOURCES_DIR})
   include(${DCMTK_SOURCES_DIR}/CMake/CheckFunctionWithHeaderExists.cmake)
   include(${DCMTK_SOURCES_DIR}/CMake/GenerateDCMTKConfigure.cmake)
 
-  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-    set(HAVE_SSTREAM 1)
-    set(HAVE_PROTOTYPE_BZERO 1)
-    set(HAVE_PROTOTYPE_GETHOSTNAME 1)
-    set(HAVE_PROTOTYPE_GETSOCKOPT 1)
-    set(HAVE_PROTOTYPE_SETSOCKOPT 1)
-    set(HAVE_PROTOTYPE_CONNECT 1)
-    set(HAVE_PROTOTYPE_BIND 1)
-    set(HAVE_PROTOTYPE_ACCEPT 1)
-    set(HAVE_PROTOTYPE_SETSOCKNAME 1)
-    set(HAVE_PROTOTYPE_GETSOCKNAME 1)
+
+  if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")  # WebAssembly or
+    # asm.js The macros below are not properly discovered by DCMTK
+    # when using WebAssembly. Check out "../WebAssembly/arith.h" for
+    # how we produced these values. This step MUST be after
+    # "GenerateDCMTKConfigure" and before the generation of
+    # "osconfig.h".
+    UNSET(SIZEOF_VOID_P   CACHE)
+    UNSET(SIZEOF_CHAR     CACHE)
+    UNSET(SIZEOF_DOUBLE   CACHE)
+    UNSET(SIZEOF_FLOAT    CACHE)
+    UNSET(SIZEOF_INT      CACHE)
+    UNSET(SIZEOF_LONG     CACHE)
+    UNSET(SIZEOF_SHORT    CACHE)
+    UNSET(SIZEOF_VOID_P   CACHE)
+
+    SET(SIZEOF_VOID_P 4   CACHE INTERNAL "")
+    SET(SIZEOF_CHAR 1     CACHE INTERNAL "")
+    SET(SIZEOF_DOUBLE 8   CACHE INTERNAL "")
+    SET(SIZEOF_FLOAT 4    CACHE INTERNAL "")
+    SET(SIZEOF_INT 4      CACHE INTERNAL "")
+    SET(SIZEOF_LONG 4     CACHE INTERNAL "")
+    SET(SIZEOF_SHORT 2    CACHE INTERNAL "")
+    SET(SIZEOF_VOID_P 4   CACHE INTERNAL "")
   endif()
 
+
   set(DCMTK_PACKAGE_VERSION_SUFFIX "")
   set(DCMTK_PACKAGE_VERSION_NUMBER ${DCMTK_VERSION_NUMBER})
 
@@ -55,12 +179,37 @@
     ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in
     ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h)
 
-  AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmnet/libsrc DCMTK_SOURCES)
+  if (NOT USE_DCMTK_360)
+    if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+      link_libraries(netapi32)  # For NetWkstaUserGetInfo@12
+      link_libraries(iphlpapi)  # For GetAdaptersInfo@8
+
+      # Configure Wine if cross-compiling for Windows
+      if (CMAKE_COMPILER_IS_GNUCXX)
+        include(${DCMTK_SOURCES_DIR}/CMake/dcmtkUseWine.cmake)
+        FIND_PROGRAM(WINE_WINE_PROGRAM wine)
+        FIND_PROGRAM(WINE_WINEPATH_PROGRAM winepath)
+        list(APPEND DCMTK_TRY_COMPILE_REQUIRED_CMAKE_FLAGS "-DCMAKE_EXE_LINKER_FLAGS=-static")
+      endif()
+    endif()
+
+    # This step must be after the generation of "osconfig.h"
+    if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+      INSPECT_FUNDAMENTAL_ARITHMETIC_TYPES()
+    endif()
+  endif()
+
   AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmdata/libsrc DCMTK_SOURCES)
   AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/ofstd/libsrc DCMTK_SOURCES)
 
+  if (ENABLE_DCMTK_NETWORKING)
+    AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmnet/libsrc DCMTK_SOURCES)
+    include_directories(
+      ${DCMTK_SOURCES_DIR}/dcmnet/include
+      )
+  endif()
 
-  if (ENABLE_JPEG)
+  if (ENABLE_DCMTK_JPEG)
     AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc DCMTK_SOURCES)
     AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpeg/libijg8 DCMTK_SOURCES)
     AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpeg/libijg12 DCMTK_SOURCES)
@@ -74,11 +223,21 @@
       )
     list(REMOVE_ITEM DCMTK_SOURCES 
       ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/ddpiimpl.cc
+
+      # Disable support for encoding JPEG (modification in Orthanc 1.0.1)
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djcodece.cc
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencsv1.cc
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencbas.cc
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencpro.cc
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djenclol.cc
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencode.cc
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencext.cc
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencsps.cc
       )
   endif()
 
 
-  if (ENABLE_JPEG_LOSSLESS)
+  if (ENABLE_DCMTK_JPEG_LOSSLESS)
     AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpls/libsrc DCMTK_SOURCES)
     AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpls/libcharls DCMTK_SOURCES)
     include_directories(
@@ -88,6 +247,9 @@
       )
     list(REMOVE_ITEM DCMTK_SOURCES 
       ${DCMTK_SOURCES_DIR}/dcmjpls/libsrc/djcodece.cc
+
+      # Disable support for encoding JPEG-LS (modification in Orthanc 1.0.1)
+      ${DCMTK_SOURCES_DIR}/dcmjpls/libsrc/djencode.cc
       )
     list(APPEND DCMTK_SOURCES 
       ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djrplol.cc
@@ -100,31 +262,27 @@
   if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
+      ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten")
     list(REMOVE_ITEM DCMTK_SOURCES 
+      ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc
       ${DCMTK_SOURCES_DIR}/oflog/libsrc/windebap.cc
       ${DCMTK_SOURCES_DIR}/oflog/libsrc/winsock.cc
       )
-    
-    execute_process(
-      COMMAND ${PATCH_EXECUTABLE} -p0 -N -i ${ORTHANC_ROOT}/Resources/Patches/dcmtk-linux-speed.patch
-      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-      RESULT_VARIABLE Failure
-      )
-
-    if (Failure AND FirstRun)
-      message(FATAL_ERROR "Error while patching a file")
-    endif()
 
   elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
     list(REMOVE_ITEM DCMTK_SOURCES 
       ${DCMTK_SOURCES_DIR}/oflog/libsrc/unixsock.cc
+      ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc
       )
 
-    if (CMAKE_COMPILER_IS_GNUCXX)
+    if (CMAKE_COMPILER_IS_GNUCXX AND
+        USE_DCMTK_360)
       # This is a patch for MinGW64
       execute_process(
-        COMMAND ${PATCH_EXECUTABLE} -p0 -N -i ${ORTHANC_ROOT}/Resources/Patches/dcmtk-mingw64.patch
+        COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+        ${ORTHANC_ROOT}/Resources/Patches/dcmtk-3.6.0-mingw64.patch
         WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
         RESULT_VARIABLE Failure
         )
@@ -133,24 +291,26 @@
         message(FATAL_ERROR "Error while patching a file")
       endif()
     endif()
-
-    # This patch improves speed, even for Windows
-    execute_process(
-      COMMAND ${PATCH_EXECUTABLE} -p0 -N 
-      INPUT_FILE ${ORTHANC_ROOT}/Resources/Patches/dcmtk-linux-speed.patch
-      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-      RESULT_VARIABLE Failure
-      )
-
-    if (Failure AND FirstRun)
-      message(FATAL_ERROR "Error while patching a file")
-    endif()
   endif()
 
+
+  if (NOT USE_DCMTK_360 AND
+      ORTHANC_SANDBOXED)
+    configure_file(
+      ${ORTHANC_ROOT}/Resources/WebAssembly/dcdict.h
+      ${DCMTK_SOURCES_DIR}/dcmdata/include/dcmtk/dcmdata/dcdict.h
+      COPYONLY)
+    
+    configure_file(
+      ${ORTHANC_ROOT}/Resources/WebAssembly/dcdict.cc
+      ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict.cc
+      COPYONLY)
+  endif()
+
+  
   list(REMOVE_ITEM DCMTK_SOURCES 
     ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/mkdictbi.cc
     ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/mkdeftag.cc
-    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdictbi.cc
     )
 
   #set_source_files_properties(${DCMTK_SOURCES}
@@ -167,7 +327,6 @@
   include_directories(
     #${DCMTK_SOURCES_DIR}
     ${DCMTK_SOURCES_DIR}/config/include
-    ${DCMTK_SOURCES_DIR}/dcmnet/include
     ${DCMTK_SOURCES_DIR}/ofstd/include
     ${DCMTK_SOURCES_DIR}/oflog/include
     ${DCMTK_SOURCES_DIR}/dcmdata/include
@@ -178,17 +337,16 @@
   set(DCMTK_BUNDLES_LOG4CPLUS 1)
 
   if (STANDALONE_BUILD)
-    add_definitions(-DDCMTK_USE_EMBEDDED_DICTIONARIES=1)
+    set(DCMTK_USE_EMBEDDED_DICTIONARIES 1)
+    set(DCMTK_DICTIONARIES
+      DICTIONARY_DICOM ${DCMTK_SOURCES_DIR}/dcmdata/data/dicom.dic
+      DICTIONARY_PRIVATE ${DCMTK_SOURCES_DIR}/dcmdata/data/private.dic
+      DICTIONARY_DICONDE ${DCMTK_SOURCES_DIR}/dcmdata/data/diconde.dic
+      )
   else()
-    add_definitions(-DDCMTK_USE_EMBEDDED_DICTIONARIES=0)
+    set(DCMTK_USE_EMBEDDED_DICTIONARIES 0)
   endif()
 
-  set(DCMTK_DICTIONARIES
-    DICTIONARY_DICOM ${DCMTK_SOURCES_DIR}/dcmdata/data/dicom.dic
-    DICTIONARY_PRIVATE ${DCMTK_SOURCES_DIR}/dcmdata/data/private.dic
-    DICTIONARY_DICONDE ${DCMTK_SOURCES_DIR}/dcmdata/data/diconde.dic
-    )
-
 else()
   # The following line allows to manually add libraries at the
   # command-line, which is necessary for Ubuntu/Debian packages
@@ -196,20 +354,24 @@
   include(FindDCMTK)
   list(APPEND DCMTK_LIBRARIES "${tmp}")
 
-  include_directories(${DCMTK_INCLUDE_DIR})
+  include_directories(${DCMTK_INCLUDE_DIRS})
 
   add_definitions(
     -DHAVE_CONFIG_H=1
     )
 
-  if (EXISTS "${DCMTK_DIR}/config/cfunix.h")
-    set(DCMTK_CONFIGURATION_FILE "${DCMTK_DIR}/config/cfunix.h")
-  elseif (EXISTS "${DCMTK_DIR}/config/osconfig.h")  # This is for Arch Linux
-    set(DCMTK_CONFIGURATION_FILE "${DCMTK_DIR}/config/osconfig.h")
+  if (EXISTS "${DCMTK_config_INCLUDE_DIR}/cfunix.h")
+    set(DCMTK_CONFIGURATION_FILE "${DCMTK_config_INCLUDE_DIR}/cfunix.h")
+  elseif (EXISTS "${DCMTK_config_INCLUDE_DIR}/osconfig.h")  # This is for Arch Linux
+    set(DCMTK_CONFIGURATION_FILE "${DCMTK_config_INCLUDE_DIR}/osconfig.h")
+  elseif (EXISTS "${DCMTK_INCLUDE_DIRS}/dcmtk/config/osconfig.h")  # This is for Debian Buster
+    set(DCMTK_CONFIGURATION_FILE "${DCMTK_INCLUDE_DIRS}/dcmtk/config/osconfig.h")
   else()
-    message(FATAL_ERROR "Please install libdcmtk1-dev")
+    message(FATAL_ERROR "Please install libdcmtk*-dev")
   endif()
 
+  message("DCMTK configuration file: ${DCMTK_CONFIGURATION_FILE}")
+  
   # Autodetection of the version of DCMTK
   file(STRINGS
     "${DCMTK_CONFIGURATION_FILE}" 
@@ -222,9 +384,50 @@
     DCMTK_VERSION_NUMBER 
     ${DCMTK_VERSION_NUMBER1})
 
-  add_definitions(-DDCMTK_USE_EMBEDDED_DICTIONARIES=0)
+  set(DCMTK_USE_EMBEDDED_DICTIONARIES 0)
+endif()
 
-endif()
 
 add_definitions(-DDCMTK_VERSION_NUMBER=${DCMTK_VERSION_NUMBER})
 message("DCMTK version: ${DCMTK_VERSION_NUMBER}")
+
+
+add_definitions(-DDCMTK_USE_EMBEDDED_DICTIONARIES=${DCMTK_USE_EMBEDDED_DICTIONARIES})
+if (NOT DCMTK_USE_EMBEDDED_DICTIONARIES)
+  # Lookup for DICOM dictionaries, if none is specified by the user
+  if (DCMTK_DICTIONARY_DIR STREQUAL "")
+    find_path(DCMTK_DICTIONARY_DIR_AUTO dicom.dic
+      /usr/share/dcmtk
+      /usr/share/libdcmtk1
+      /usr/share/libdcmtk2
+      /usr/share/libdcmtk3
+      /usr/share/libdcmtk4
+      /usr/share/libdcmtk5
+      /usr/share/libdcmtk6
+      /usr/share/libdcmtk7
+      /usr/share/libdcmtk8
+      /usr/share/libdcmtk9
+      /usr/share/libdcmtk10
+      /usr/share/libdcmtk11
+      /usr/share/libdcmtk12
+      /usr/share/libdcmtk13
+      /usr/share/libdcmtk14
+      /usr/share/libdcmtk15
+      /usr/share/libdcmtk16
+      /usr/share/libdcmtk17
+      /usr/share/libdcmtk18
+      /usr/share/libdcmtk19
+      /usr/share/libdcmtk20
+      /usr/local/share/dcmtk
+      )
+
+    if (${DCMTK_DICTIONARY_DIR_AUTO} MATCHES "DCMTK_DICTIONARY_DIR_AUTO-NOTFOUND")
+      message(FATAL_ERROR "Cannot locate the DICOM dictionary on this system")
+    endif()
+
+    message("Autodetected path to the DICOM dictionaries: ${DCMTK_DICTIONARY_DIR_AUTO}")
+    add_definitions(-DDCMTK_DICTIONARY_DIR="${DCMTK_DICTIONARY_DIR_AUTO}")
+  else()
+    add_definitions(-DDCMTK_DICTIONARY_DIR="${DCMTK_DICTIONARY_DIR}")
+  endif()
+endif()
--- a/Resources/CMake/DownloadPackage.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/DownloadPackage.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -15,12 +15,18 @@
 ## Setup the patch command-line tool
 ##
 
-if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
-  set(PATCH_EXECUTABLE ${CMAKE_SOURCE_DIR}/Resources/ThirdParty/patch/patch.exe)
-else ()
-  find_program(PATCH_EXECUTABLE patch)
-  if (${PATCH_EXECUTABLE} MATCHES "PATCH_EXECUTABLE-NOTFOUND")
-    message(FATAL_ERROR "Please install the 'patch' standard command-line tool")
+if (NOT ORTHANC_DISABLE_PATCH)
+  if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
+    set(PATCH_EXECUTABLE ${CMAKE_CURRENT_LIST_DIR}/../ThirdParty/patch/patch.exe)
+    if (NOT EXISTS ${PATCH_EXECUTABLE})
+      message(FATAL_ERROR "Unable to find the patch.exe tool that is shipped with Orthanc")
+    endif()
+
+  else ()
+    find_program(PATCH_EXECUTABLE patch)
+    if (${PATCH_EXECUTABLE} MATCHES "PATCH_EXECUTABLE-NOTFOUND")
+      message(FATAL_ERROR "Please install the 'patch' standard command-line tool")
+    endif()
   endif()
 endif()
 
@@ -70,9 +76,24 @@
 	message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON")
       endif()
 
-      file(DOWNLOAD "${Url}" "${TMP_PATH}" SHOW_PROGRESS EXPECTED_MD5 "${MD5}")
+      if ("${MD5}" STREQUAL "no-check")
+        message(WARNING "Not checking the MD5 of: ${Url}")
+        file(DOWNLOAD "${Url}" "${TMP_PATH}" SHOW_PROGRESS TIMEOUT 60 INACTIVITY_TIMEOUT 60)
+      else()
+        file(DOWNLOAD "${Url}" "${TMP_PATH}" SHOW_PROGRESS TIMEOUT 60 INACTIVITY_TIMEOUT 60 EXPECTED_MD5 "${MD5}")
+      endif()
+
     else()
       message("Using local copy of ${Url}")
+
+      if ("${MD5}" STREQUAL "no-check")
+        message(WARNING "Not checking the MD5 of: ${Url}")
+      else()
+        file(MD5 ${TMP_PATH} ActualMD5)
+        if (NOT "${ActualMD5}" STREQUAL "${MD5}")
+          message(FATAL_ERROR "The MD5 hash of a previously download file is invalid: ${TMP_PATH}")
+        endif()
+      endif()
     endif()
 
     GetUrlExtension(TMP_EXTENSION "${Url}")
@@ -83,7 +104,9 @@
       # How to silently extract files using 7-zip
       # http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly
 
-      if (("${TMP_EXTENSION}" STREQUAL "gz") OR ("${TMP_EXTENSION}" STREQUAL "tgz"))
+      if (("${TMP_EXTENSION}" STREQUAL "gz") OR 
+          ("${TMP_EXTENSION}" STREQUAL "tgz") OR
+          ("${TMP_EXTENSION}" STREQUAL "xz"))
         execute_process(
           COMMAND ${ZIP_EXECUTABLE} e -y ${TMP_PATH}
           WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
@@ -97,8 +120,10 @@
 
         if ("${TMP_EXTENSION}" STREQUAL "tgz")
           string(REGEX REPLACE ".tgz$" ".tar" TMP_FILENAME2 "${TMP_FILENAME}")
-        else()
+        elseif ("${TMP_EXTENSION}" STREQUAL "gz")
           string(REGEX REPLACE ".gz$" "" TMP_FILENAME2 "${TMP_FILENAME}")
+        elseif ("${TMP_EXTENSION}" STREQUAL "xz")
+          string(REGEX REPLACE ".xz" "" TMP_FILENAME2 "${TMP_FILENAME}")
         endif()
 
         execute_process(
@@ -138,6 +163,12 @@
           WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
           RESULT_VARIABLE Failure
           )
+      elseif ("${TMP_EXTENSION}" STREQUAL "xz")
+        execute_process(
+          COMMAND sh -c "${TAR_EXECUTABLE} xf ${TMP_PATH}"
+          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+          RESULT_VARIABLE Failure
+          )
       else()
         message(FATAL_ERROR "Unknown package format.")
       endif()
--- a/Resources/CMake/GoogleLogConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,197 +0,0 @@
-if (STATIC_BUILD OR NOT USE_SYSTEM_GOOGLE_LOG)
-  SET(GOOGLE_LOG_SOURCES_DIR ${CMAKE_BINARY_DIR}/glog-0.3.2)
-  SET(GOOGLE_LOG_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/glog-0.3.2.tar.gz")
-  SET(GOOGLE_LOG_MD5 "897fbff90d91ea2b6d6e78c8cea641cc")
-
-  if (IS_DIRECTORY "${GOOGLE_LOG_SOURCES_DIR}")
-    set(FirstRun OFF)
-  else()
-    set(FirstRun ON)
-  endif()
-
-  DownloadPackage(${GOOGLE_LOG_MD5} ${GOOGLE_LOG_URL} "${GOOGLE_LOG_SOURCES_DIR}")
-
-  # Glog 0.3.3 fails to build with old versions of MinGW, such as the
-  # one installed on our Continuous Integration Server that runs
-  # Debian Squeeze. We thus stick to Glog 0.3.2 for the time being.
-
-  #SET(GOOGLE_LOG_SOURCES_DIR ${CMAKE_BINARY_DIR}/glog-0.3.3)
-  #DownloadPackage(
-  #  "a6fd2c22f8996846e34c763422717c18"
-  #  "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/glog-0.3.3.tar.gz"
-  #  "${GOOGLE_LOG_SOURCES_DIR}")
-
-
-  set(GOOGLE_LOG_HEADERS
-    ${GOOGLE_LOG_SOURCES_DIR}/src/glog/logging.h
-    ${GOOGLE_LOG_SOURCES_DIR}/src/glog/raw_logging.h
-    ${GOOGLE_LOG_SOURCES_DIR}/src/glog/stl_logging.h
-    ${GOOGLE_LOG_SOURCES_DIR}/src/glog/vlog_is_on.h
-    )
-
-  set(ac_google_namespace google)
-  set(ac_google_start_namespace "namespace google {")
-  set(ac_google_end_namespace "}")
-
-  if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
-    set(ac_cv_have_unistd_h 1)
-    set(ac_cv_have_stdint_h 1)
-    set(ac_cv_have_systypes_h 0)
-    set(ac_cv_have_inttypes_h 0)
-    set(ac_cv_have_libgflags 0)
-    set(ac_cv_have_uint16_t 1)
-    set(ac_cv_have_u_int16_t 0)
-    set(ac_cv_have___uint16 0)
-    set(ac_cv_cxx_using_operator 1)
-    set(ac_cv_have___builtin_expect 1)
-  else()
-    set(ac_cv_have_unistd_h 0)
-    set(ac_cv_have_stdint_h 0)
-    set(ac_cv_have_systypes_h 0)
-    set(ac_cv_have_inttypes_h 0)
-    set(ac_cv_have_libgflags 0)
-    set(ac_cv_have_uint16_t 0)
-    set(ac_cv_have_u_int16_t 0)
-    set(ac_cv_have___uint16 1)
-    set(ac_cv_cxx_using_operator 1)
-    set(ac_cv_have___builtin_expect 0)
-  endif()
-
-  foreach (f ${GOOGLE_LOG_HEADERS})
-    configure_file(${f}.in ${f})
-  endforeach()
-
-  include_directories(
-    ${GOOGLE_LOG_SOURCES_DIR}/src
-    )
-
-  if (CMAKE_COMPILER_IS_GNUCXX)
-    if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-      execute_process(
-        COMMAND ${PATCH_EXECUTABLE} -N utilities.cc -i ${ORTHANC_ROOT}/Resources/Patches/glog-utilities-lsb.diff
-        WORKING_DIRECTORY ${GOOGLE_LOG_SOURCES_DIR}/src
-        RESULT_VARIABLE Failure
-        )
-    else()
-      execute_process(
-        COMMAND ${PATCH_EXECUTABLE} -N utilities.cc -i ${ORTHANC_ROOT}/Resources/Patches/glog-utilities.diff
-        WORKING_DIRECTORY ${GOOGLE_LOG_SOURCES_DIR}/src
-        RESULT_VARIABLE Failure
-        )
-    endif()
-
-    if (Failure AND FirstRun)
-      message(FATAL_ERROR "Error while patching a file")
-    endif()
-
-    # Patches for MinGW
-    execute_process(
-      #COMMAND ${PATCH_EXECUTABLE} -N port.h -i ${ORTHANC_ROOT}/Resources/Patches/glog-port-h.diff 
-      COMMAND ${PATCH_EXECUTABLE} -N port.h -i ${ORTHANC_ROOT}/Resources/Patches/glog-port-h-v2.diff 
-      WORKING_DIRECTORY ${GOOGLE_LOG_SOURCES_DIR}/src/windows
-      RESULT_VARIABLE Failure
-      )
-
-    if (Failure AND FirstRun)
-      message(FATAL_ERROR "Error while patching a file")
-    endif()
-
-    execute_process(
-      COMMAND ${PATCH_EXECUTABLE} -N port.cc -i ${ORTHANC_ROOT}/Resources/Patches/glog-port-cc.diff 
-      WORKING_DIRECTORY ${GOOGLE_LOG_SOURCES_DIR}/src/windows
-      RESULT_VARIABLE Failure
-      )
-
-    if (Failure AND FirstRun)
-      message(FATAL_ERROR "Error while patching a file")
-    endif()
-
-  elseif (MSVC)
-    # https://code.google.com/p/google-glog/issues/detail?id=117
-    configure_file(
-      ${ORTHANC_ROOT}/Resources/Patches/glog-visual-studio-port.h
-      ${GOOGLE_LOG_SOURCES_DIR}/src/windows/port.h
-      COPYONLY)
-
-  endif()
-
-
-  if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
-    if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-      # Install the specific configuration for LSB SDK
-      configure_file(
-        ${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfigurationLSB.h
-        ${GOOGLE_LOG_SOURCES_DIR}/src/config.h
-        COPYONLY)
-    elseif ("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin")
-      # Install the specific configuration for Mac OS
-      configure_file(
-        ${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfigurationDarwin.h
-        ${GOOGLE_LOG_SOURCES_DIR}/src/config.h
-        COPYONLY)
-    else()
-      configure_file(
-        ${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfiguration.h
-        ${GOOGLE_LOG_SOURCES_DIR}/src/config.h
-        COPYONLY)
-    endif()
-
-    set(GOOGLE_LOG_SOURCES
-      ${GOOGLE_LOG_SOURCES_DIR}/src/demangle.cc
-      ${GOOGLE_LOG_SOURCES_DIR}/src/logging.cc
-      ${GOOGLE_LOG_SOURCES_DIR}/src/raw_logging.cc
-      ${GOOGLE_LOG_SOURCES_DIR}/src/signalhandler.cc
-      ${GOOGLE_LOG_SOURCES_DIR}/src/symbolize.cc
-      ${GOOGLE_LOG_SOURCES_DIR}/src/utilities.cc
-      ${GOOGLE_LOG_SOURCES_DIR}/src/vlog_is_on.cc
-      )
-
-  elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-    include_directories(
-      ${GOOGLE_LOG_SOURCES_DIR}/src/windows
-      )
-
-    set(GOOGLE_LOG_SOURCES
-      ${GOOGLE_LOG_SOURCES_DIR}/src/windows/port.cc
-      ${GOOGLE_LOG_SOURCES_DIR}/src/logging.cc
-      ${GOOGLE_LOG_SOURCES_DIR}/src/raw_logging.cc
-      ${GOOGLE_LOG_SOURCES_DIR}/src/utilities.cc
-      ${GOOGLE_LOG_SOURCES_DIR}/src/vlog_is_on.cc
-      )
-
-    add_definitions(
-      -DGLOG_NO_ABBREVIATED_SEVERITIES=1
-      -DNO_FRAME_POINTER=1
-      -DGOOGLE_GLOG_DLL_DECL=
-      )
-
-    if (CMAKE_COMPILER_IS_GNUCXX)
-      # This is a patch for MinGW64
-      add_definitions(-D_TIME_H__S=1)
-    endif()
-  endif()
-
-else()
-  CHECK_INCLUDE_FILE_CXX(glog/logging.h HAVE_GOOGLE_LOG_H)
-  if (NOT HAVE_GOOGLE_LOG_H)
-    message(FATAL_ERROR "Please install the libgoogle-glog-dev package")
-  endif()
-
-  link_libraries(glog)
-endif()
-
-
-CHECK_INCLUDE_FILES(sec_api/string_s.h HAVE_SECURE_STRING_EXTENSIONS)
-if (HAVE_SECURE_STRING_EXTENSIONS)
-  add_definitions(-DHAVE_SECURE_STRING_EXTENSIONS=1)
-else()
-  add_definitions(-DHAVE_SECURE_STRING_EXTENSIONS=0)
-endif()
-
-add_definitions(-DORTHANC_ENABLE_GOOGLE_LOG=1)
--- a/Resources/CMake/GoogleLogConfiguration.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,175 +0,0 @@
-/* src/config.h.  Generated from config.h.in by configure.  */
-/* src/config.h.in.  Generated from configure.ac by autoheader.  */
-
-/* Namespace for Google classes */
-#define GOOGLE_NAMESPACE google
-
-/* Define if you have the `dladdr' function */
-/* #undef HAVE_DLADDR */
-
-/* Define to 1 if you have the <dlfcn.h> header file. */
-#define HAVE_DLFCN_H 1
-
-/* Define to 1 if you have the <execinfo.h> header file. */
-#define HAVE_EXECINFO_H 1
-
-/* Define if you have the `fcntl' function */
-#define HAVE_FCNTL 1
-
-/* Define to 1 if you have the <glob.h> header file. */
-#define HAVE_GLOB_H 1
-
-/* Define to 1 if you have the <inttypes.h> header file. */
-#define HAVE_INTTYPES_H 1
-
-/* Define to 1 if you have the `pthread' library (-lpthread). */
-#define HAVE_LIBPTHREAD 1
-
-/* Define to 1 if you have the <libunwind.h> header file. */
-/* #undef HAVE_LIBUNWIND_H */
-
-/* define if you have google gflags library */
-/* #undef HAVE_LIB_GFLAGS */
-
-/* define if you have google gmock library */
-/* #undef HAVE_LIB_GMOCK */
-
-/* define if you have google gtest library */
-/* #undef HAVE_LIB_GTEST */
-
-/* define if you have libunwind */
-/* #undef HAVE_LIB_UNWIND */
-
-/* Define to 1 if you have the <memory.h> header file. */
-#define HAVE_MEMORY_H 1
-
-/* define if the compiler implements namespaces */
-#define HAVE_NAMESPACES 1
-
-/* Define if you have POSIX threads libraries and header files. */
-#define HAVE_PTHREAD 1
-
-/* Define to 1 if you have the <pwd.h> header file. */
-#define HAVE_PWD_H 1
-
-/* define if the compiler implements pthread_rwlock_* */
-#define HAVE_RWLOCK 1
-
-/* Define if you have the `sigaltstack' function */
-#define HAVE_SIGALTSTACK 1
-
-/* Define to 1 if you have the <stdint.h> header file. */
-#define HAVE_STDINT_H 1
-
-/* Define to 1 if you have the <stdlib.h> header file. */
-#define HAVE_STDLIB_H 1
-
-/* Define to 1 if you have the <strings.h> header file. */
-#define HAVE_STRINGS_H 1
-
-/* Define to 1 if you have the <string.h> header file. */
-#define HAVE_STRING_H 1
-
-/* Define to 1 if you have the <syscall.h> header file. */
-#define HAVE_SYSCALL_H 1
-
-/* Define to 1 if you have the <syslog.h> header file. */
-#define HAVE_SYSLOG_H 1
-
-/* Define to 1 if you have the <sys/stat.h> header file. */
-#define HAVE_SYS_STAT_H 1
-
-/* Define to 1 if you have the <sys/syscall.h> header file. */
-#define HAVE_SYS_SYSCALL_H 1
-
-/* Define to 1 if you have the <sys/time.h> header file. */
-#define HAVE_SYS_TIME_H 1
-
-/* Define to 1 if you have the <sys/types.h> header file. */
-#define HAVE_SYS_TYPES_H 1
-
-/* Define to 1 if you have the <sys/ucontext.h> header file. */
-#define HAVE_SYS_UCONTEXT_H 1
-
-/* Define to 1 if you have the <sys/utsname.h> header file. */
-#define HAVE_SYS_UTSNAME_H 1
-
-/* Define to 1 if you have the <ucontext.h> header file. */
-#define HAVE_UCONTEXT_H 1
-
-/* Define to 1 if you have the <unistd.h> header file. */
-#define HAVE_UNISTD_H 1
-
-/* define if the compiler supports using expression for operator */
-#define HAVE_USING_OPERATOR 1
-
-/* define if your compiler has __attribute__ */
-#define HAVE___ATTRIBUTE__ 1
-
-/* define if your compiler has __builtin_expect */
-#define HAVE___BUILTIN_EXPECT 1
-
-/* define if your compiler has __sync_val_compare_and_swap */
-#define HAVE___SYNC_VAL_COMPARE_AND_SWAP 1
-
-/* Define to the sub-directory in which libtool stores uninstalled libraries.
-   */
-#define LT_OBJDIR ".libs/"
-
-/* Name of package */
-#define PACKAGE "glog"
-
-/* Define to the address where bug reports for this package should be sent. */
-#define PACKAGE_BUGREPORT "opensource@google.com"
-
-/* Define to the full name of this package. */
-#define PACKAGE_NAME "glog"
-
-/* Define to the full name and version of this package. */
-#define PACKAGE_STRING "glog 0.3.2"
-
-/* Define to the one symbol short name of this package. */
-#define PACKAGE_TARNAME "glog"
-
-/* Define to the home page for this package. */
-#define PACKAGE_URL ""
-
-/* Define to the version of this package. */
-#define PACKAGE_VERSION "0.3.2"
-
-/* How to access the PC from a struct ucontext */
-/*#include <ucontext.h>
-#include <sys/ucontext.h>
-#ifdef REG_RIP
-#define PC_FROM_UCONTEXT uc_mcontext.gregs[REG_RIP]
-#else
-#undef PC_FROM_UCONTEXT
-#endif*/
-
-// This is required for older versions of Linux
-#undef PC_FROM_UCONTEXT
-
-/* Define to necessary symbol if this constant uses a non-standard name on
-   your system. */
-/* #undef PTHREAD_CREATE_JOINABLE */
-
-/* The size of `void *', as computed by sizeof. */
-#define SIZEOF_VOID_P 8
-
-/* Define to 1 if you have the ANSI C header files. */
-/* #undef STDC_HEADERS */
-
-/* the namespace where STL code like vector<> is defined */
-#define STL_NAMESPACE std
-
-/* location of source code */
-#define TEST_SRC_DIR "."
-
-/* Version number of package */
-#define VERSION "0.3.2"
-
-/* Stops putting the code inside the Google namespace */
-#define _END_GOOGLE_NAMESPACE_ }
-
-/* Puts following code inside the Google namespace */
-#define _START_GOOGLE_NAMESPACE_ namespace google {
--- a/Resources/CMake/GoogleLogConfigurationDarwin.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,175 +0,0 @@
-/* src/config.h.  Generated from config.h.in by configure.  */
-/* src/config.h.in.  Generated from configure.ac by autoheader.  */
-
-/* Namespace for Google classes */
-#define GOOGLE_NAMESPACE google
-
-/* Define if you have the `dladdr' function */
-/* #undef HAVE_DLADDR */
-
-/* Define to 1 if you have the <dlfcn.h> header file. */
-#define HAVE_DLFCN_H 1
-
-/* Define to 1 if you have the <execinfo.h> header file. */
-#define HAVE_EXECINFO_H 1
-
-/* Define if you have the `fcntl' function */
-#define HAVE_FCNTL 1
-
-/* Define to 1 if you have the <glob.h> header file. */
-#define HAVE_GLOB_H 1
-
-/* Define to 1 if you have the <inttypes.h> header file. */
-#define HAVE_INTTYPES_H 1
-
-/* Define to 1 if you have the `pthread' library (-lpthread). */
-#define HAVE_LIBPTHREAD 1
-
-/* Define to 1 if you have the <libunwind.h> header file. */
-/* #undef HAVE_LIBUNWIND_H */
-
-/* define if you have google gflags library */
-/* #undef HAVE_LIB_GFLAGS */
-
-/* define if you have google gmock library */
-/* #undef HAVE_LIB_GMOCK */
-
-/* define if you have google gtest library */
-/* #undef HAVE_LIB_GTEST */
-
-/* define if you have libunwind */
-/* #undef HAVE_LIB_UNWIND */
-
-/* Define to 1 if you have the <memory.h> header file. */
-#define HAVE_MEMORY_H 1
-
-/* define if the compiler implements namespaces */
-#define HAVE_NAMESPACES 1
-
-/* Define if you have POSIX threads libraries and header files. */
-#define HAVE_PTHREAD 1
-
-/* Define to 1 if you have the <pwd.h> header file. */
-#define HAVE_PWD_H 1
-
-/* define if the compiler implements pthread_rwlock_* */
-#define HAVE_RWLOCK 1
-
-/* Define if you have the `sigaltstack' function */
-#define HAVE_SIGALTSTACK 1
-
-/* Define to 1 if you have the <stdint.h> header file. */
-#define HAVE_STDINT_H 1
-
-/* Define to 1 if you have the <stdlib.h> header file. */
-#define HAVE_STDLIB_H 1
-
-/* Define to 1 if you have the <strings.h> header file. */
-#define HAVE_STRINGS_H 1
-
-/* Define to 1 if you have the <string.h> header file. */
-#define HAVE_STRING_H 1
-
-/* Define to 1 if you have the <syscall.h> header file. */
-/* #undef HAVE_SYSCALL_H */
-
-/* Define to 1 if you have the <syslog.h> header file. */
-#define HAVE_SYSLOG_H 1
-
-/* Define to 1 if you have the <sys/stat.h> header file. */
-/* #define HAVE_SYS_STAT_H 1 */
-
-/* Define to 1 if you have the <sys/syscall.h> header file. */
-#define HAVE_SYS_SYSCALL_H 1
-
-/* Define to 1 if you have the <sys/time.h> header file. */
-#define HAVE_SYS_TIME_H 1
-
-/* Define to 1 if you have the <sys/types.h> header file. */
-/* #define HAVE_SYS_TYPES_H 1 */
-
-/* Define to 1 if you have the <sys/ucontext.h> header file. */
-/* #define HAVE_SYS_UCONTEXT_H 1 */
-
-/* Define to 1 if you have the <sys/utsname.h> header file. */
-#define HAVE_SYS_UTSNAME_H 1
-
-/* Define to 1 if you have the <ucontext.h> header file. */
-#define HAVE_UCONTEXT_H 1
-
-/* Define to 1 if you have the <unistd.h> header file. */
-#define HAVE_UNISTD_H 1
-
-/* define if the compiler supports using expression for operator */
-#define HAVE_USING_OPERATOR 1
-
-/* define if your compiler has __attribute__ */
-#define HAVE___ATTRIBUTE__ 1
-
-/* define if your compiler has __builtin_expect */
-#define HAVE___BUILTIN_EXPECT 1
-
-/* define if your compiler has __sync_val_compare_and_swap */
-#define HAVE___SYNC_VAL_COMPARE_AND_SWAP 1
-
-/* Define to the sub-directory in which libtool stores uninstalled libraries.
-   */
-#define LT_OBJDIR ".libs/"
-
-/* Name of package */
-#define PACKAGE "glog"
-
-/* Define to the address where bug reports for this package should be sent. */
-#define PACKAGE_BUGREPORT "opensource@google.com"
-
-/* Define to the full name of this package. */
-#define PACKAGE_NAME "glog"
-
-/* Define to the full name and version of this package. */
-#define PACKAGE_STRING "glog 0.3.2"
-
-/* Define to the one symbol short name of this package. */
-#define PACKAGE_TARNAME "glog"
-
-/* Define to the home page for this package. */
-#define PACKAGE_URL ""
-
-/* Define to the version of this package. */
-#define PACKAGE_VERSION "0.3.2"
-
-/* How to access the PC from a struct ucontext */
-/*#include <ucontext.h>
-#include <sys/ucontext.h>
-#ifdef REG_RIP
-#define PC_FROM_UCONTEXT uc_mcontext.gregs[REG_RIP]
-#else
-#undef PC_FROM_UCONTEXT
-#endif*/
-
-// This is required for older versions of Linux
-#undef PC_FROM_UCONTEXT
-
-/* Define to necessary symbol if this constant uses a non-standard name on
-   your system. */
-/* #undef PTHREAD_CREATE_JOINABLE */
-
-/* The size of `void *', as computed by sizeof. */
-#define SIZEOF_VOID_P 8
-
-/* Define to 1 if you have the ANSI C header files. */
-/* #undef STDC_HEADERS */
-
-/* the namespace where STL code like vector<> is defined */
-#define STL_NAMESPACE std
-
-/* location of source code */
-#define TEST_SRC_DIR "."
-
-/* Version number of package */
-#define VERSION "0.3.2"
-
-/* Stops putting the code inside the Google namespace */
-#define _END_GOOGLE_NAMESPACE_ }
-
-/* Puts following code inside the Google namespace */
-#define _START_GOOGLE_NAMESPACE_ namespace google {
--- a/Resources/CMake/GoogleLogConfigurationLSB.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,175 +0,0 @@
-/* src/config.h.  Generated from config.h.in by configure.  */
-/* src/config.h.in.  Generated from configure.ac by autoheader.  */
-
-/* Namespace for Google classes */
-#define GOOGLE_NAMESPACE google
-
-/* Define if you have the `dladdr' function */
-/* #undef HAVE_DLADDR */
-
-/* Define to 1 if you have the <dlfcn.h> header file. */
-#define HAVE_DLFCN_H 1
-
-/* Define to 1 if you have the <execinfo.h> header file. */
-#define HAVE_EXECINFO_H 1
-
-/* Define if you have the `fcntl' function */
-#define HAVE_FCNTL 1
-
-/* Define to 1 if you have the <glob.h> header file. */
-#define HAVE_GLOB_H 1
-
-/* Define to 1 if you have the <inttypes.h> header file. */
-#define HAVE_INTTYPES_H 1
-
-/* Define to 1 if you have the `pthread' library (-lpthread). */
-#define HAVE_LIBPTHREAD 1
-
-/* Define to 1 if you have the <libunwind.h> header file. */
-/* #undef HAVE_LIBUNWIND_H */
-
-/* define if you have google gflags library */
-/* #undef HAVE_LIB_GFLAGS */
-
-/* define if you have google gmock library */
-/* #undef HAVE_LIB_GMOCK */
-
-/* define if you have google gtest library */
-/* #undef HAVE_LIB_GTEST */
-
-/* define if you have libunwind */
-/* #undef HAVE_LIB_UNWIND */
-
-/* Define to 1 if you have the <memory.h> header file. */
-#define HAVE_MEMORY_H 1
-
-/* define if the compiler implements namespaces */
-#define HAVE_NAMESPACES 1
-
-/* Define if you have POSIX threads libraries and header files. */
-#define HAVE_PTHREAD 1
-
-/* Define to 1 if you have the <pwd.h> header file. */
-#define HAVE_PWD_H 1
-
-/* define if the compiler implements pthread_rwlock_* */
-#define HAVE_RWLOCK 1
-
-/* Define if you have the `sigaltstack' function */
-#define HAVE_SIGALTSTACK 1
-
-/* Define to 1 if you have the <stdint.h> header file. */
-#define HAVE_STDINT_H 1
-
-/* Define to 1 if you have the <stdlib.h> header file. */
-#define HAVE_STDLIB_H 1
-
-/* Define to 1 if you have the <strings.h> header file. */
-#define HAVE_STRINGS_H 1
-
-/* Define to 1 if you have the <string.h> header file. */
-#define HAVE_STRING_H 1
-
-/* Define to 1 if you have the <syscall.h> header file. */
-/* #undef HAVE_SYSCALL_H */
-
-/* Define to 1 if you have the <syslog.h> header file. */
-#define HAVE_SYSLOG_H 1
-
-/* Define to 1 if you have the <sys/stat.h> header file. */
-#define HAVE_SYS_STAT_H 1
-
-/* Define to 1 if you have the <sys/syscall.h> header file. */
-/* #undef HAVE_SYS_SYSCALL_H */
-
-/* Define to 1 if you have the <sys/time.h> header file. */
-#define HAVE_SYS_TIME_H 1
-
-/* Define to 1 if you have the <sys/types.h> header file. */
-#define HAVE_SYS_TYPES_H 1
-
-/* Define to 1 if you have the <sys/ucontext.h> header file. */
-/* #define HAVE_SYS_UCONTEXT_H 1 */
-
-/* Define to 1 if you have the <sys/utsname.h> header file. */
-#define HAVE_SYS_UTSNAME_H 1
-
-/* Define to 1 if you have the <ucontext.h> header file. */
-/* #define HAVE_UCONTEXT_H 1 */
-
-/* Define to 1 if you have the <unistd.h> header file. */
-#define HAVE_UNISTD_H 1
-
-/* define if the compiler supports using expression for operator */
-#define HAVE_USING_OPERATOR 1
-
-/* define if your compiler has __attribute__ */
-#define HAVE___ATTRIBUTE__ 1
-
-/* define if your compiler has __builtin_expect */
-#define HAVE___BUILTIN_EXPECT 1
-
-/* define if your compiler has __sync_val_compare_and_swap */
-#define HAVE___SYNC_VAL_COMPARE_AND_SWAP 1
-
-/* Define to the sub-directory in which libtool stores uninstalled libraries.
-   */
-#define LT_OBJDIR ".libs/"
-
-/* Name of package */
-#define PACKAGE "glog"
-
-/* Define to the address where bug reports for this package should be sent. */
-#define PACKAGE_BUGREPORT "opensource@google.com"
-
-/* Define to the full name of this package. */
-#define PACKAGE_NAME "glog"
-
-/* Define to the full name and version of this package. */
-#define PACKAGE_STRING "glog 0.3.2"
-
-/* Define to the one symbol short name of this package. */
-#define PACKAGE_TARNAME "glog"
-
-/* Define to the home page for this package. */
-#define PACKAGE_URL ""
-
-/* Define to the version of this package. */
-#define PACKAGE_VERSION "0.3.2"
-
-/* How to access the PC from a struct ucontext */
-/*#include <ucontext.h>
-#include <sys/ucontext.h>
-#ifdef REG_RIP
-#define PC_FROM_UCONTEXT uc_mcontext.gregs[REG_RIP]
-#else
-#undef PC_FROM_UCONTEXT
-#endif*/
-
-// This is required for older versions of Linux
-#undef PC_FROM_UCONTEXT
-
-/* Define to necessary symbol if this constant uses a non-standard name on
-   your system. */
-/* #undef PTHREAD_CREATE_JOINABLE */
-
-/* The size of `void *', as computed by sizeof. */
-#define SIZEOF_VOID_P 8
-
-/* Define to 1 if you have the ANSI C header files. */
-/* #undef STDC_HEADERS */
-
-/* the namespace where STL code like vector<> is defined */
-#define STL_NAMESPACE std
-
-/* location of source code */
-#define TEST_SRC_DIR "."
-
-/* Version number of package */
-#define VERSION "0.3.2"
-
-/* Stops putting the code inside the Google namespace */
-#define _END_GOOGLE_NAMESPACE_ }
-
-/* Puts following code inside the Google namespace */
-#define _START_GOOGLE_NAMESPACE_ namespace google {
--- a/Resources/CMake/GoogleTestConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/GoogleTestConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,32 +1,58 @@
-if (USE_GTEST_DEBIAN_SOURCE_PACKAGE)
-  set(GTEST_SOURCES /usr/src/gtest/src/gtest-all.cc)
-  include_directories(/usr/src/gtest)
+if (USE_GOOGLE_TEST_DEBIAN_PACKAGE)
+  find_path(GOOGLE_TEST_DEBIAN_SOURCES_DIR
+    NAMES src/gtest-all.cc
+    PATHS
+    /usr/src/gtest
+    /usr/src/googletest/googletest
+    PATH_SUFFIXES src
+    )
 
-  if (NOT EXISTS /usr/include/gtest/gtest.h OR
-      NOT EXISTS ${GTEST_SOURCES})
+  find_path(GOOGLE_TEST_DEBIAN_INCLUDE_DIR
+    NAMES gtest.h
+    PATHS
+    /usr/include/gtest
+    )
+
+  message("Path to the Debian Google Test sources: ${GOOGLE_TEST_DEBIAN_SOURCES_DIR}")
+  message("Path to the Debian Google Test includes: ${GOOGLE_TEST_DEBIAN_INCLUDE_DIR}")
+
+  set(GOOGLE_TEST_SOURCES
+    ${GOOGLE_TEST_DEBIAN_SOURCES_DIR}/src/gtest-all.cc
+    )
+
+  include_directories(${GOOGLE_TEST_DEBIAN_SOURCES_DIR})
+
+  if (NOT EXISTS ${GOOGLE_TEST_SOURCES} OR
+      NOT EXISTS ${GOOGLE_TEST_DEBIAN_INCLUDE_DIR}/gtest.h)
     message(FATAL_ERROR "Please install the libgtest-dev package")
   endif()
 
 elseif (STATIC_BUILD OR NOT USE_SYSTEM_GOOGLE_TEST)
-  set(GTEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/gtest-1.7.0)
-  set(GTEST_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/gtest-1.7.0.zip")
-  set(GTEST_MD5 "2d6ec8ccdf5c46b05ba54a9fd1d130d7")
+  set(GOOGLE_TEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/gtest-1.7.0)
+  set(GOOGLE_TEST_URL "http://www.orthanc-server.com/downloads/third-party/gtest-1.7.0.zip")
+  set(GOOGLE_TEST_MD5 "2d6ec8ccdf5c46b05ba54a9fd1d130d7")
 
-  DownloadPackage(${GTEST_MD5} ${GTEST_URL} "${GTEST_SOURCES_DIR}")
+  DownloadPackage(${GOOGLE_TEST_MD5} ${GOOGLE_TEST_URL} "${GOOGLE_TEST_SOURCES_DIR}")
 
   include_directories(
-    ${GTEST_SOURCES_DIR}/include
-    ${GTEST_SOURCES_DIR}
+    ${GOOGLE_TEST_SOURCES_DIR}/include
+    ${GOOGLE_TEST_SOURCES_DIR}
     )
 
-  set(GTEST_SOURCES
-    ${GTEST_SOURCES_DIR}/src/gtest-all.cc
+  set(GOOGLE_TEST_SOURCES
+    ${GOOGLE_TEST_SOURCES_DIR}/src/gtest-all.cc
     )
 
   # https://code.google.com/p/googletest/issues/detail?id=412
   if (MSVC) # VS2012 does not support tuples correctly yet
     add_definitions(/D _VARIADIC_MAX=10)
   endif()
+  
+  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
+    add_definitions(-DGTEST_HAS_CLONE=0)
+  endif()
+  
+  source_group(ThirdParty\\GoogleTest REGULAR_EXPRESSION ${GOOGLE_TEST_SOURCES_DIR}/.*)
 
 else()
   include(FindGTest)
@@ -35,5 +61,8 @@
   endif()
 
   include_directories(${GTEST_INCLUDE_DIRS})
-  link_libraries(${GTEST_LIBRARIES})
+
+  # The variable GTEST_LIBRARIES contains the shared library of
+  # Google Test, create an alias for more uniformity
+  set(GOOGLE_TEST_LIBRARIES ${GTEST_LIBRARIES})
 endif()
--- a/Resources/CMake/JsonCppConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/JsonCppConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,18 @@
+set(JSONCPP_CXX11 OFF)
+
 if (STATIC_BUILD OR NOT USE_SYSTEM_JSONCPP)
-  set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-0.10.5)
-  set(JSONCPP_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/jsoncpp-0.10.5.tar.gz")
-  set(JSONCPP_MD5 "db146bac5a126ded9bd728ab7b61ed6b")
+  if (USE_LEGACY_JSONCPP)
+    set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-0.10.6)
+    set(JSONCPP_URL "http://www.orthanc-server.com/downloads/third-party/jsoncpp-0.10.6.tar.gz")
+    set(JSONCPP_MD5 "13d1991d79697df8cadbc25c93e37c83")
+    add_definitions(-DORTHANC_LEGACY_JSONCPP=1)
+  else()
+    set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-1.8.4)
+    set(JSONCPP_URL "http://www.orthanc-server.com/downloads/third-party/jsoncpp-1.8.4.tar.gz")
+    set(JSONCPP_MD5 "fa47a3ab6b381869b6a5f20811198662")
+    add_definitions(-DORTHANC_LEGACY_JSONCPP=0)
+    set(JSONCPP_CXX11 ON)
+  endif()
 
   DownloadPackage(${JSONCPP_MD5} ${JSONCPP_URL} "${JSONCPP_SOURCES_DIR}")
 
@@ -15,6 +26,10 @@
     ${JSONCPP_SOURCES_DIR}/include
     )
 
+  if (NOT ENABLE_LOCALE)
+    add_definitions(-DJSONCPP_NO_LOCALE_SUPPORT=1)
+  endif()
+    
   source_group(ThirdParty\\JsonCpp REGULAR_EXPRESSION ${JSONCPP_SOURCES_DIR}/.*)
 
 else()
@@ -32,4 +47,40 @@
     message(FATAL_ERROR "Please install the libjsoncpp-dev package")
   endif()
 
+  # Switch to the C++11 standard if the version of JsonCpp is 1.y.z
+  if (EXISTS ${JSONCPP_INCLUDE_DIR}/json/version.h)
+    file(STRINGS
+      "${JSONCPP_INCLUDE_DIR}/json/version.h" 
+      JSONCPP_VERSION_MAJOR1 REGEX
+      ".*define JSONCPP_VERSION_MAJOR.*")
+
+    if (NOT JSONCPP_VERSION_MAJOR1)
+      message(FATAL_ERROR "Unable to extract the major version of JsonCpp")
+    endif()
+    
+    string(REGEX REPLACE
+      ".*JSONCPP_VERSION_MAJOR.*([0-9]+)$" "\\1" 
+      JSONCPP_VERSION_MAJOR ${JSONCPP_VERSION_MAJOR1})
+    message("JsonCpp major version: ${JSONCPP_VERSION_MAJOR}")
+
+    if (JSONCPP_VERSION_MAJOR GREATER 0)
+      set(JSONCPP_CXX11 ON)
+    endif()
+  else()
+    message("Unable to detect the major version of JsonCpp, assuming < 1.0.0")
+  endif()
 endif()
+
+
+if (JSONCPP_CXX11)
+  # Osimis has encountered problems when this macro is left at its
+  # default value (1000), so we increase this limit
+  # https://gitlab.kitware.com/third-party/jsoncpp/commit/56df2068470241f9043b676bfae415ed62a0c172
+  add_definitions(-DJSONCPP_DEPRECATED_STACK_LIMIT=5000)
+
+  if (CMAKE_COMPILER_IS_GNUCXX OR
+      "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
+    message("Switching to C++11 standard in gcc/clang, as version of JsonCpp is >= 1.0.0")
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wno-deprecated-declarations")
+  endif()
+endif()
--- a/Resources/CMake/LibCurlConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/LibCurlConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,15 +1,35 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_CURL)
-  SET(CURL_SOURCES_DIR ${CMAKE_BINARY_DIR}/curl-7.44.0)
-  SET(CURL_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/curl-7.44.0.tar.gz")
-  SET(CURL_MD5 "cf46112b5151e2f1a3fd38439bdade23")
+  SET(CURL_SOURCES_DIR ${CMAKE_BINARY_DIR}/curl-7.57.0)
+  SET(CURL_URL "http://www.orthanc-server.com/downloads/third-party/curl-7.57.0.tar.gz")
+  SET(CURL_MD5 "c7aab73aaf5e883ca1d7518f93649dc2")
 
+  if (IS_DIRECTORY "${CURL_SOURCES_DIR}")
+    set(FirstRun OFF)
+  else()
+    set(FirstRun ON)
+  endif()
+  
   DownloadPackage(${CURL_MD5} ${CURL_URL} "${CURL_SOURCES_DIR}")
 
+  if (FirstRun)
+    execute_process(
+      COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+      ${ORTHANC_ROOT}/Resources/Patches/curl-7.57.0-cmake.patch
+      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+      RESULT_VARIABLE Failure
+      )
+    
+    if (Failure)
+      message(FATAL_ERROR "Error while patching a file")
+    endif()
+  endif()
+  
   include_directories(
     ${CURL_SOURCES_DIR}/include
     )
 
   AUX_SOURCE_DIRECTORY(${CURL_SOURCES_DIR}/lib CURL_SOURCES)
+  AUX_SOURCE_DIRECTORY(${CURL_SOURCES_DIR}/lib/vauth CURL_SOURCES)
   AUX_SOURCE_DIRECTORY(${CURL_SOURCES_DIR}/lib/vtls CURL_SOURCES)
   source_group(ThirdParty\\LibCurl REGULAR_EXPRESSION ${CURL_SOURCES_DIR}/.*)
 
@@ -35,22 +55,32 @@
     add_definitions(
       #-DHAVE_LIBSSL=1
       -DUSE_OPENSSL=1
+      -DHAVE_OPENSSL_ENGINE_H=1
       -DUSE_SSLEAY=1
       )
   endif()
 
-  file(WRITE ${CURL_SOURCES_DIR}/lib/curl_config.h "")
+  if (NOT EXISTS "${CURL_SOURCES_DIR}/lib/curl_config.h")
+    #file(WRITE ${CURL_SOURCES_DIR}/lib/curl_config.h "")
+
+    file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/vauth/vauth.h "#include \"../vauth.h\"\n")
+    file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/vauth/digest.h "#include \"../digest.h\"\n")
+    file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/vauth/ntlm.h "#include \"../ntlm.h\"\n")
+    file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/vtls/vtls.h "#include \"../../vtls/vtls.h\"\n")
 
-  file(GLOB CURL_LIBS_HEADERS ${CURL_SOURCES_DIR}/lib/*.h)
-  foreach (header IN LISTS CURL_LIBS_HEADERS)
-    get_filename_component(filename ${header} NAME)
-    file(WRITE ${CURL_SOURCES_DIR}/lib/vtls/${filename} "#include \"../${filename}\"\n")
-  endforeach()
+    file(GLOB CURL_LIBS_HEADERS ${CURL_SOURCES_DIR}/lib/*.h)
+    foreach (header IN LISTS CURL_LIBS_HEADERS)
+      get_filename_component(filename ${header} NAME)
+      file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/${filename} "#include \"../${filename}\"\n")
+      file(WRITE ${CURL_SOURCES_DIR}/lib/vtls/${filename} "#include \"../${filename}\"\n")
+    endforeach()
+  endif()
 
   if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
+      ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
     if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
       SET(TMP_OS "x86_64")
     else()
@@ -59,48 +89,229 @@
 
     set_property(
       SOURCE ${CURL_SOURCES}
-      PROPERTY COMPILE_DEFINITIONS "HAVE_TIME_H;HAVE_STRUCT_TIMEVAL;HAVE_SYS_STAT_H;HAVE_SOCKET;HAVE_STRUCT_SOCKADDR_STORAGE;HAVE_SYS_SOCKET_H;HAVE_SOCKET;HAVE_SYS_SOCKET_H;HAVE_NETINET_IN_H;HAVE_NETDB_H;HAVE_FCNTL_O_NONBLOCK;HAVE_FCNTL_H;HAVE_SELECT;HAVE_ERRNO_H;HAVE_SEND;HAVE_RECV;HAVE_LONGLONG;OS=\"${TMP_OS}\""
+      PROPERTY COMPILE_DEFINITIONS "HAVE_CONFIG_H=1;OS=\"${TMP_OS}\""
       )
+   
+    include(${CURL_SOURCES_DIR}/CMake/Macros.cmake)
+
+    # WARNING: Do *not* reorder the "check_include_file_concat()" below!
+    check_include_file_concat("stdio.h"          HAVE_STDIO_H)
+    check_include_file_concat("inttypes.h"       HAVE_INTTYPES_H)
+    check_include_file_concat("sys/filio.h"      HAVE_SYS_FILIO_H)
+    check_include_file_concat("sys/ioctl.h"      HAVE_SYS_IOCTL_H)
+    check_include_file_concat("sys/param.h"      HAVE_SYS_PARAM_H)
+    check_include_file_concat("sys/poll.h"       HAVE_SYS_POLL_H)
+    check_include_file_concat("sys/resource.h"   HAVE_SYS_RESOURCE_H)
+    check_include_file_concat("sys/select.h"     HAVE_SYS_SELECT_H)
+    check_include_file_concat("sys/socket.h"     HAVE_SYS_SOCKET_H)
+    check_include_file_concat("sys/sockio.h"     HAVE_SYS_SOCKIO_H)
+    check_include_file_concat("sys/stat.h"       HAVE_SYS_STAT_H)
+    check_include_file_concat("sys/time.h"       HAVE_SYS_TIME_H)
+    check_include_file_concat("sys/types.h"      HAVE_SYS_TYPES_H)
+    check_include_file_concat("sys/uio.h"        HAVE_SYS_UIO_H)
+    check_include_file_concat("sys/un.h"         HAVE_SYS_UN_H)
+    check_include_file_concat("sys/utime.h"      HAVE_SYS_UTIME_H)
+    check_include_file_concat("sys/xattr.h"      HAVE_SYS_XATTR_H)
+    check_include_file_concat("alloca.h"         HAVE_ALLOCA_H)
+    check_include_file_concat("arpa/inet.h"      HAVE_ARPA_INET_H)
+    check_include_file_concat("arpa/tftp.h"      HAVE_ARPA_TFTP_H)
+    check_include_file_concat("assert.h"         HAVE_ASSERT_H)
+    check_include_file_concat("crypto.h"         HAVE_CRYPTO_H)
+    check_include_file_concat("des.h"            HAVE_DES_H)
+    check_include_file_concat("err.h"            HAVE_ERR_H)
+    check_include_file_concat("errno.h"          HAVE_ERRNO_H)
+    check_include_file_concat("fcntl.h"          HAVE_FCNTL_H)
+    check_include_file_concat("idn2.h"           HAVE_IDN2_H)
+    check_include_file_concat("ifaddrs.h"        HAVE_IFADDRS_H)
+    check_include_file_concat("io.h"             HAVE_IO_H)
+    check_include_file_concat("krb.h"            HAVE_KRB_H)
+    check_include_file_concat("libgen.h"         HAVE_LIBGEN_H)
+    check_include_file_concat("limits.h"         HAVE_LIMITS_H)
+    check_include_file_concat("locale.h"         HAVE_LOCALE_H)
+    check_include_file_concat("net/if.h"         HAVE_NET_IF_H)
+    check_include_file_concat("netdb.h"          HAVE_NETDB_H)
+    check_include_file_concat("netinet/in.h"     HAVE_NETINET_IN_H)
+    check_include_file_concat("netinet/tcp.h"    HAVE_NETINET_TCP_H)
+
+    check_include_file_concat("pem.h"            HAVE_PEM_H)
+    check_include_file_concat("poll.h"           HAVE_POLL_H)
+    check_include_file_concat("pwd.h"            HAVE_PWD_H)
+    check_include_file_concat("rsa.h"            HAVE_RSA_H)
+    check_include_file_concat("setjmp.h"         HAVE_SETJMP_H)
+    check_include_file_concat("sgtty.h"          HAVE_SGTTY_H)
+    check_include_file_concat("signal.h"         HAVE_SIGNAL_H)
+    check_include_file_concat("ssl.h"            HAVE_SSL_H)
+    check_include_file_concat("stdbool.h"        HAVE_STDBOOL_H)
+    check_include_file_concat("stdint.h"         HAVE_STDINT_H)
+    check_include_file_concat("stdio.h"          HAVE_STDIO_H)
+    check_include_file_concat("stdlib.h"         HAVE_STDLIB_H)
+    check_include_file_concat("string.h"         HAVE_STRING_H)
+    check_include_file_concat("strings.h"        HAVE_STRINGS_H)
+    check_include_file_concat("stropts.h"        HAVE_STROPTS_H)
+    check_include_file_concat("termio.h"         HAVE_TERMIO_H)
+    check_include_file_concat("termios.h"        HAVE_TERMIOS_H)
+    check_include_file_concat("time.h"           HAVE_TIME_H)
+    check_include_file_concat("unistd.h"         HAVE_UNISTD_H)
+    check_include_file_concat("utime.h"          HAVE_UTIME_H)
+    check_include_file_concat("x509.h"           HAVE_X509_H)
+
+    check_include_file_concat("process.h"        HAVE_PROCESS_H)
+    check_include_file_concat("stddef.h"         HAVE_STDDEF_H)
+    check_include_file_concat("dlfcn.h"          HAVE_DLFCN_H)
+    check_include_file_concat("malloc.h"         HAVE_MALLOC_H)
+    check_include_file_concat("memory.h"         HAVE_MEMORY_H)
+    check_include_file_concat("netinet/if_ether.h" HAVE_NETINET_IF_ETHER_H)
+    check_include_file_concat("stdint.h"        HAVE_STDINT_H)
+    check_include_file_concat("sockio.h"        HAVE_SOCKIO_H)
+    check_include_file_concat("sys/utsname.h"   HAVE_SYS_UTSNAME_H)
+
+    check_type_size("size_t"  SIZEOF_SIZE_T)
+    check_type_size("ssize_t"  SIZEOF_SSIZE_T)
+    check_type_size("long long"  SIZEOF_LONG_LONG)
+    check_type_size("long"  SIZEOF_LONG)
+    check_type_size("short"  SIZEOF_SHORT)
+    check_type_size("int"  SIZEOF_INT)
+    check_type_size("__int64"  SIZEOF___INT64)
+    check_type_size("long double"  SIZEOF_LONG_DOUBLE)
+    check_type_size("time_t"  SIZEOF_TIME_T)
+    check_type_size("off_t"  SIZEOF_OFF_T)
+    check_type_size("socklen_t" CURL_SIZEOF_CURL_SOCKLEN_T)
 
-    if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
-      add_definitions(
-        -DRECV_TYPE_ARG1=int
-        -DRECV_TYPE_ARG2=void*
-        -DRECV_TYPE_ARG3=size_t
-        -DRECV_TYPE_ARG4=int
-        -DRECV_TYPE_RETV=ssize_t
-        -DSEND_TYPE_ARG1=int
-        -DSEND_TYPE_ARG2=void*
-        -DSEND_QUAL_ARG2=const
-        -DSEND_TYPE_ARG3=size_t
-        -DSEND_TYPE_ARG4=int
-        -DSEND_TYPE_RETV=ssize_t
-        -DSIZEOF_SHORT=2
-        -DSIZEOF_INT=4
-        -DSIZEOF_SIZE_T=8
+    check_symbol_exists(basename      "${CURL_INCLUDES}" HAVE_BASENAME)
+    check_symbol_exists(socket        "${CURL_INCLUDES}" HAVE_SOCKET)
+    # poll on macOS is unreliable, it first did not exist, then was broken until
+    # fixed in 10.9 only to break again in 10.12.
+    if(NOT APPLE)
+      check_symbol_exists(poll        "${CURL_INCLUDES}" HAVE_POLL)
+    endif()
+    check_symbol_exists(select        "${CURL_INCLUDES}" HAVE_SELECT)
+    check_symbol_exists(strdup        "${CURL_INCLUDES}" HAVE_STRDUP)
+    check_symbol_exists(strstr        "${CURL_INCLUDES}" HAVE_STRSTR)
+    check_symbol_exists(strtok_r      "${CURL_INCLUDES}" HAVE_STRTOK_R)
+    check_symbol_exists(strftime      "${CURL_INCLUDES}" HAVE_STRFTIME)
+    check_symbol_exists(uname         "${CURL_INCLUDES}" HAVE_UNAME)
+    check_symbol_exists(strcasecmp    "${CURL_INCLUDES}" HAVE_STRCASECMP)
+    check_symbol_exists(stricmp       "${CURL_INCLUDES}" HAVE_STRICMP)
+    check_symbol_exists(strcmpi       "${CURL_INCLUDES}" HAVE_STRCMPI)
+    check_symbol_exists(strncmpi      "${CURL_INCLUDES}" HAVE_STRNCMPI)
+    check_symbol_exists(alarm         "${CURL_INCLUDES}" HAVE_ALARM)
+    if(NOT HAVE_STRNCMPI)
+      set(HAVE_STRCMPI)
+    endif(NOT HAVE_STRNCMPI)
+
+    check_symbol_exists(gethostbyaddr "${CURL_INCLUDES}" HAVE_GETHOSTBYADDR)
+    check_symbol_exists(gethostbyaddr_r "${CURL_INCLUDES}" HAVE_GETHOSTBYADDR_R)
+    check_symbol_exists(gettimeofday  "${CURL_INCLUDES}" HAVE_GETTIMEOFDAY)
+    check_symbol_exists(inet_addr     "${CURL_INCLUDES}" HAVE_INET_ADDR)
+    check_symbol_exists(inet_ntoa     "${CURL_INCLUDES}" HAVE_INET_NTOA)
+    check_symbol_exists(inet_ntoa_r   "${CURL_INCLUDES}" HAVE_INET_NTOA_R)
+    check_symbol_exists(tcsetattr     "${CURL_INCLUDES}" HAVE_TCSETATTR)
+    check_symbol_exists(tcgetattr     "${CURL_INCLUDES}" HAVE_TCGETATTR)
+    check_symbol_exists(perror        "${CURL_INCLUDES}" HAVE_PERROR)
+    check_symbol_exists(closesocket   "${CURL_INCLUDES}" HAVE_CLOSESOCKET)
+    check_symbol_exists(setvbuf       "${CURL_INCLUDES}" HAVE_SETVBUF)
+    check_symbol_exists(sigsetjmp     "${CURL_INCLUDES}" HAVE_SIGSETJMP)
+    check_symbol_exists(getpass_r     "${CURL_INCLUDES}" HAVE_GETPASS_R)
+    check_symbol_exists(strlcat       "${CURL_INCLUDES}" HAVE_STRLCAT)
+    check_symbol_exists(getpwuid      "${CURL_INCLUDES}" HAVE_GETPWUID)
+    check_symbol_exists(geteuid       "${CURL_INCLUDES}" HAVE_GETEUID)
+    check_symbol_exists(utime         "${CURL_INCLUDES}" HAVE_UTIME)
+    check_symbol_exists(gmtime_r      "${CURL_INCLUDES}" HAVE_GMTIME_R)
+    check_symbol_exists(localtime_r   "${CURL_INCLUDES}" HAVE_LOCALTIME_R)
+
+    check_symbol_exists(gethostbyname   "${CURL_INCLUDES}" HAVE_GETHOSTBYNAME)
+    check_symbol_exists(gethostbyname_r "${CURL_INCLUDES}" HAVE_GETHOSTBYNAME_R)
+
+    check_symbol_exists(signal        "${CURL_INCLUDES}" HAVE_SIGNAL_FUNC)
+    check_symbol_exists(SIGALRM       "${CURL_INCLUDES}" HAVE_SIGNAL_MACRO)
+    if(HAVE_SIGNAL_FUNC AND HAVE_SIGNAL_MACRO)
+      set(HAVE_SIGNAL 1)
+    endif(HAVE_SIGNAL_FUNC AND HAVE_SIGNAL_MACRO)
+    check_symbol_exists(uname          "${CURL_INCLUDES}" HAVE_UNAME)
+    check_symbol_exists(strtoll        "${CURL_INCLUDES}" HAVE_STRTOLL)
+    check_symbol_exists(_strtoi64      "${CURL_INCLUDES}" HAVE__STRTOI64)
+    check_symbol_exists(strerror_r     "${CURL_INCLUDES}" HAVE_STRERROR_R)
+    check_symbol_exists(siginterrupt   "${CURL_INCLUDES}" HAVE_SIGINTERRUPT)
+    check_symbol_exists(perror         "${CURL_INCLUDES}" HAVE_PERROR)
+    check_symbol_exists(fork           "${CURL_INCLUDES}" HAVE_FORK)
+    check_symbol_exists(getaddrinfo    "${CURL_INCLUDES}" HAVE_GETADDRINFO)
+    check_symbol_exists(freeaddrinfo   "${CURL_INCLUDES}" HAVE_FREEADDRINFO)
+    check_symbol_exists(freeifaddrs    "${CURL_INCLUDES}" HAVE_FREEIFADDRS)
+    check_symbol_exists(pipe           "${CURL_INCLUDES}" HAVE_PIPE)
+    check_symbol_exists(ftruncate      "${CURL_INCLUDES}" HAVE_FTRUNCATE)
+    check_symbol_exists(getprotobyname "${CURL_INCLUDES}" HAVE_GETPROTOBYNAME)
+    check_symbol_exists(getrlimit      "${CURL_INCLUDES}" HAVE_GETRLIMIT)
+    check_symbol_exists(setlocale      "${CURL_INCLUDES}" HAVE_SETLOCALE)
+    check_symbol_exists(setmode        "${CURL_INCLUDES}" HAVE_SETMODE)
+    check_symbol_exists(setrlimit      "${CURL_INCLUDES}" HAVE_SETRLIMIT)
+    check_symbol_exists(fcntl          "${CURL_INCLUDES}" HAVE_FCNTL)
+    check_symbol_exists(ioctl          "${CURL_INCLUDES}" HAVE_IOCTL)
+    check_symbol_exists(setsockopt     "${CURL_INCLUDES}" HAVE_SETSOCKOPT)
+
+    if(HAVE_SIZEOF_LONG_LONG)
+      set(HAVE_LONGLONG 1)
+      set(HAVE_LL 1)
+    endif(HAVE_SIZEOF_LONG_LONG)
+
+    check_function_exists(mach_absolute_time HAVE_MACH_ABSOLUTE_TIME)
+    check_function_exists(gethostname HAVE_GETHOSTNAME)
+
+    check_include_file_concat("pthread.h" HAVE_PTHREAD_H)
+    check_symbol_exists(recv "sys/socket.h" HAVE_RECV)
+    check_symbol_exists(send "sys/socket.h" HAVE_SEND)
+
+    check_struct_has_member("struct sockaddr_un" sun_path "sys/un.h" USE_UNIX_SOCKETS)
+
+    set(CMAKE_REQUIRED_INCLUDES "${CURL_SOURCES_DIR}/include")
+    set(CMAKE_EXTRA_INCLUDE_FILES "curl/system.h")
+    check_type_size("curl_off_t"  SIZEOF_CURL_OFF_T)
+
+    add_definitions(-DHAVE_GLIBC_STRERROR_R=1)
+
+    include(${CURL_SOURCES_DIR}/CMake/OtherTests.cmake)
+
+    foreach(CURL_TEST
+        HAVE_FCNTL_O_NONBLOCK
+        HAVE_IOCTLSOCKET
+        HAVE_IOCTLSOCKET_CAMEL
+        HAVE_IOCTLSOCKET_CAMEL_FIONBIO
+        HAVE_IOCTLSOCKET_FIONBIO
+        HAVE_IOCTL_FIONBIO
+        HAVE_IOCTL_SIOCGIFADDR
+        HAVE_SETSOCKOPT_SO_NONBLOCK
+        HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID
+        TIME_WITH_SYS_TIME
+        HAVE_O_NONBLOCK
+        HAVE_GETHOSTBYADDR_R_5
+        HAVE_GETHOSTBYADDR_R_7
+        HAVE_GETHOSTBYADDR_R_8
+        HAVE_GETHOSTBYADDR_R_5_REENTRANT
+        HAVE_GETHOSTBYADDR_R_7_REENTRANT
+        HAVE_GETHOSTBYADDR_R_8_REENTRANT
+        HAVE_GETHOSTBYNAME_R_3
+        HAVE_GETHOSTBYNAME_R_5
+        HAVE_GETHOSTBYNAME_R_6
+        HAVE_GETHOSTBYNAME_R_3_REENTRANT
+        HAVE_GETHOSTBYNAME_R_5_REENTRANT
+        HAVE_GETHOSTBYNAME_R_6_REENTRANT
+        HAVE_SOCKLEN_T
+        HAVE_IN_ADDR_T
+        HAVE_BOOL_T
+        STDC_HEADERS
+        RETSIGTYPE_TEST
+        HAVE_INET_NTOA_R_DECL
+        HAVE_INET_NTOA_R_DECL_REENTRANT
+        HAVE_GETADDRINFO
+        HAVE_FILE_OFFSET_BITS
         )
-    elseif ("${CMAKE_SIZEOF_VOID_P}" EQUAL "4")
-      add_definitions(
-        -DRECV_TYPE_ARG1=int
-        -DRECV_TYPE_ARG2=void*
-        -DRECV_TYPE_ARG3=size_t
-        -DRECV_TYPE_ARG4=int
-        -DRECV_TYPE_RETV=int
-        -DSEND_TYPE_ARG1=int
-        -DSEND_TYPE_ARG2=void*
-        -DSEND_QUAL_ARG2=const
-        -DSEND_TYPE_ARG3=size_t
-        -DSEND_TYPE_ARG4=int
-        -DSEND_TYPE_RETV=int
-        -DSIZEOF_SHORT=2
-        -DSIZEOF_INT=4
-        -DSIZEOF_SIZE_T=4
-        )
-    else()
-      message(FATAL_ERROR "Support your platform here")
-    endif()
+      curl_internal_test(${CURL_TEST})
+    endforeach(CURL_TEST)
+
+    configure_file(
+      ${CURL_SOURCES_DIR}/lib/curl_config.h.cmake
+      ${CURL_SOURCES_DIR}/lib/curl_config.h
+      )
   endif()
-
 else()
   include(FindCURL)
   include_directories(${CURL_INCLUDE_DIRS})
--- a/Resources/CMake/LibIconvConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/LibIconvConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,50 +1,98 @@
-set(LIBICONV_SOURCES_DIR ${CMAKE_BINARY_DIR}/libiconv-1.14)
-set(LIBICONV_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/libiconv-1.14.tar.gz")
-set(LIBICONV_MD5 "e34509b1623cec449dfeb73d7ce9c6c6")
+if (NOT ENABLE_LOCALE)
+  message("Support for locales is disabled")
+
+elseif (NOT USE_BOOST_ICONV)
+  message("Not using libiconv")
 
-DownloadPackage(${LIBICONV_MD5} ${LIBICONV_URL} "${LIBICONV_SOURCES_DIR}")
+else()
+  message("Using libiconv")
+
+  if (STATIC_BUILD OR NOT USE_SYSTEM_LIBICONV)
+    set(LIBICONV_SOURCES_DIR ${CMAKE_BINARY_DIR}/libiconv-1.15)
+    set(LIBICONV_URL "http://www.orthanc-server.com/downloads/third-party/libiconv-1.15.tar.gz")
+    set(LIBICONV_MD5 "ace8b5f2db42f7b3b3057585e80d9808")
+
+    DownloadPackage(${LIBICONV_MD5} ${LIBICONV_URL} "${LIBICONV_SOURCES_DIR}")
 
-# https://groups.google.com/d/msg/android-ndk/AS1nkxnk6m4/EQm09hD1tigJ
-add_definitions(
-  -DBOOST_LOCALE_WITH_ICONV=1
-  -DBUILDING_LIBICONV=1
-  -DIN_LIBRARY=1
-  -DLIBDIR=""
-  -DICONV_CONST=
-  )
+    # Disable the support of libiconv that is shipped by default with
+    # the C standard library on Linux. Setting this macro redirects
+    # calls from "iconv*()" to "libiconv*()" by defining macros in the
+    # C headers of "libiconv-1.15".
+    add_definitions(-DLIBICONV_PLUG=1)
 
-configure_file(
-  ${LIBICONV_SOURCES_DIR}/srclib/localcharset.h
-  ${LIBICONV_SOURCES_DIR}/include
-  COPYONLY)
+    # https://groups.google.com/d/msg/android-ndk/AS1nkxnk6m4/EQm09hD1tigJ
+    add_definitions(
+      -DBUILDING_LIBICONV=1
+      -DIN_LIBRARY=1
+      -DLIBDIR=""
+      -DICONV_CONST=
+      )
+
+    configure_file(
+      ${LIBICONV_SOURCES_DIR}/srclib/localcharset.h
+      ${LIBICONV_SOURCES_DIR}/include
+      COPYONLY)
 
-set(HAVE_VISIBILITY 0)
-set(ICONV_CONST ${ICONV_CONST})
-set(USE_MBSTATE_T 1)
-set(BROKEN_WCHAR_H 0)
-set(EILSEQ)
-set(HAVE_WCHAR_T 1)   
-configure_file(
-  ${LIBICONV_SOURCES_DIR}/include/iconv.h.build.in
-  ${LIBICONV_SOURCES_DIR}/include/iconv.h
-  )
-unset(HAVE_VISIBILITY)
-unset(ICONV_CONST)
-unset(USE_MBSTATE_T)
-unset(BROKEN_WCHAR_H)
-unset(EILSEQ)
-unset(HAVE_WCHAR_T)   
+    set(HAVE_VISIBILITY 0)
+    set(ICONV_CONST ${ICONV_CONST})
+    set(USE_MBSTATE_T 1)
+    set(BROKEN_WCHAR_H 0)
+    set(EILSEQ)
+    set(HAVE_WCHAR_T 1)
+    configure_file(
+      ${LIBICONV_SOURCES_DIR}/include/iconv.h.build.in
+      ${LIBICONV_SOURCES_DIR}/include/iconv.h
+      )
+    unset(HAVE_VISIBILITY)
+    unset(ICONV_CONST)
+    unset(USE_MBSTATE_T)
+    unset(BROKEN_WCHAR_H)
+    unset(EILSEQ)
+    unset(HAVE_WCHAR_T)
+
+    if (NOT EXISTS ${LIBICONV_SOURCES_DIR}/include/config.h)
+      # Create an empty "config.h" for libiconv
+      file(WRITE ${LIBICONV_SOURCES_DIR}/include/config.h "")
+    endif()
+
+    include_directories(
+      ${LIBICONV_SOURCES_DIR}/include
+      )
 
-# Create an empty "config.h" for libiconv
-file(WRITE ${LIBICONV_SOURCES_DIR}/include/config.h "")
+    set(LIBICONV_SOURCES
+      ${LIBICONV_SOURCES_DIR}/lib/iconv.c  
+      ${LIBICONV_SOURCES_DIR}/lib/relocatable.c
+      ${LIBICONV_SOURCES_DIR}/libcharset/lib/localcharset.c  
+      ${LIBICONV_SOURCES_DIR}/libcharset/lib/relocatable.c
+      )
 
-include_directories(
-  ${LIBICONV_SOURCES_DIR}/include
-  )
+    source_group(ThirdParty\\libiconv REGULAR_EXPRESSION ${LIBICONV_SOURCES_DIR}/.*)
+
+    if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
+      add_definitions(-DHAVE_WORKING_O_NOFOLLOW=0)
+    else()
+      add_definitions(-DHAVE_WORKING_O_NOFOLLOW=1)
+    endif()
 
-list(APPEND BOOST_SOURCES
-  ${LIBICONV_SOURCES_DIR}/lib/iconv.c  
-  ${LIBICONV_SOURCES_DIR}/lib/relocatable.c
-  ${LIBICONV_SOURCES_DIR}/libcharset/lib/localcharset.c  
-  ${LIBICONV_SOURCES_DIR}/libcharset/lib/relocatable.c
-  )
+  else() 
+    CHECK_INCLUDE_FILE_CXX(iconv.h HAVE_ICONV_H)
+    if (NOT HAVE_ICONV_H)
+      message(FATAL_ERROR "Please install the libiconv-dev package")
+    endif()
+
+    # Check whether the support for libiconv is bundled within the
+    # standard C library
+    CHECK_FUNCTION_EXISTS(iconv_open HAVE_ICONV_LIB)
+    if (NOT HAVE_ICONV_LIB)
+      # No builtin support for libiconv, try and find an external library.
+      # Open question: Does this make sense on any platform?
+      CHECK_LIBRARY_EXISTS(iconv iconv_open "" HAVE_ICONV_LIB_2)
+      if (NOT HAVE_ICONV_LIB_2)
+        message(FATAL_ERROR "Please install the libiconv-dev package")
+      else()
+        link_libraries(iconv)
+      endif()
+    endif()
+
+  endif()
+endif()
--- a/Resources/CMake/LibJpegConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/LibJpegConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -2,7 +2,7 @@
   set(LIBJPEG_SOURCES_DIR ${CMAKE_BINARY_DIR}/jpeg-9a)
   DownloadPackage(
     "3353992aecaee1805ef4109aadd433e7"
-    "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/jpegsrc.v9a.tar.gz"
+    "http://www.orthanc-server.com/downloads/third-party/jpegsrc.v9a.tar.gz"
     "${LIBJPEG_SOURCES_DIR}")
 
   include_directories(
@@ -81,6 +81,8 @@
     ${LIBJPEG_SOURCES_DIR}/jconfig.h COPYONLY
     )
 
+  source_group(ThirdParty\\libjpeg REGULAR_EXPRESSION ${LIBJPEG_SOURCES_DIR}/.*)
+
 else()
   include(FindJPEG)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/LibP11Configuration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,72 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_LIBP11)
+  SET(LIBP11_SOURCES_DIR ${CMAKE_BINARY_DIR}/libp11-0.4.0)
+  SET(LIBP11_URL "http://www.orthanc-server.com/downloads/third-party/beid/libp11-0.4.0.tar.gz")
+  SET(LIBP11_MD5 "00b3e41db5be840d822bda12f3ab2ca7")
+ 
+  if (IS_DIRECTORY "${LIBP11_SOURCES_DIR}")
+    set(FirstRun OFF)
+  else()
+    set(FirstRun ON)
+  endif()
+
+  DownloadPackage(${LIBP11_MD5} ${LIBP11_URL} "${LIBP11_SOURCES_DIR}")
+
+  # Apply the patches
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Patches/libp11-0.4.0.patch
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure AND FirstRun)
+    message(FATAL_ERROR "Error while patching libp11")
+  endif()
+
+  # This command MUST be after applying the patch
+  file(COPY
+    ${LIBP11_SOURCES_DIR}/src/engine.h
+    ${LIBP11_SOURCES_DIR}/src/libp11.h
+    DESTINATION ${AUTOGENERATED_DIR}/libp11)
+
+  set(LIBP11_SOURCES 
+    #${LIBP11_SOURCES_DIR}/src/eng_front.c
+    ${LIBP11_SOURCES_DIR}/src/eng_back.c
+    ${LIBP11_SOURCES_DIR}/src/eng_parse.c
+    ${LIBP11_SOURCES_DIR}/src/libpkcs11.c
+    ${LIBP11_SOURCES_DIR}/src/p11_attr.c
+    ${LIBP11_SOURCES_DIR}/src/p11_cert.c
+    ${LIBP11_SOURCES_DIR}/src/p11_ec.c
+    ${LIBP11_SOURCES_DIR}/src/p11_err.c
+    ${LIBP11_SOURCES_DIR}/src/p11_front.c
+    ${LIBP11_SOURCES_DIR}/src/p11_key.c
+    ${LIBP11_SOURCES_DIR}/src/p11_load.c
+    ${LIBP11_SOURCES_DIR}/src/p11_misc.c
+    ${LIBP11_SOURCES_DIR}/src/p11_rsa.c
+    ${LIBP11_SOURCES_DIR}/src/p11_slot.c
+    )
+
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
+    list(APPEND LIBP11_SOURCES 
+      ${LIBP11_SOURCES_DIR}/src/atfork.c
+      )
+  endif()
+
+  source_group(ThirdParty\\libp11 REGULAR_EXPRESSION ${LIBP11_SOURCES_DIR}/.*)
+
+else()
+  check_include_file_cxx(libp11.h HAVE_LIBP11_H)
+  if (NOT HAVE_LIBP11_H)
+    message(FATAL_ERROR "Please install the libp11-dev package")
+  endif()
+
+  check_library_exists(p11 PKCS11_login "" HAVE_LIBP11_LIB)
+  if (NOT HAVE_LIBP11_LIB)
+    message(FATAL_ERROR "Please install the libp11-dev package")
+  endif()
+
+  link_libraries(p11)
+endif()
--- a/Resources/CMake/LibPngConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/LibPngConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,6 +1,6 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_LIBPNG)
   SET(LIBPNG_SOURCES_DIR ${CMAKE_BINARY_DIR}/libpng-1.5.12)
-  SET(LIBPNG_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/libpng-1.5.12.tar.gz")
+  SET(LIBPNG_URL "http://www.orthanc-server.com/downloads/third-party/libpng-1.5.12.tar.gz")
   SET(LIBPNG_MD5 "8ea7f60347a306c5faf70b977fa80e28")
 
   DownloadPackage(${LIBPNG_MD5} ${LIBPNG_URL} "${LIBPNG_SOURCES_DIR}")
@@ -46,7 +46,7 @@
     -DPNG_IMPEXP=
     )
 
-  source_group(ThirdParty\\Libpng REGULAR_EXPRESSION ${LIBPNG_SOURCES_DIR}/.*)
+  source_group(ThirdParty\\libpng REGULAR_EXPRESSION ${LIBPNG_SOURCES_DIR}/.*)
 
 else()
   include(FindPNG)
--- a/Resources/CMake/LuaConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/LuaConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,15 +1,33 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_LUA)
   SET(LUA_SOURCES_DIR ${CMAKE_BINARY_DIR}/lua-5.1.5)
   SET(LUA_MD5 "2e115fe26e435e33b0d5c022e4490567")
-  SET(LUA_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/lua-5.1.5.tar.gz")
+  SET(LUA_URL "http://www.orthanc-server.com/downloads/third-party/lua-5.1.5.tar.gz")
 
   DownloadPackage(${LUA_MD5} ${LUA_URL} "${LUA_SOURCES_DIR}")
 
-  add_definitions(
-    #-DLUA_LIB=1
-    #-Dluaall_c=1
-    #-DLUA_COMPAT_ALL=1  # Compile a generic version of Lua
-    )
+  if (ENABLE_LUA_MODULES)
+    if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
+        ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR
+        ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
+        ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+      # Enable loading of shared libraries (for UNIX-like)
+      add_definitions(-DLUA_USE_DLOPEN=1)
+
+      # Publish the functions of the Lua engine (that are built within
+      # the Orthanc binary) as global symbols, so that the external
+      # shared libraries can call them
+      set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--export-dynamic")
+
+    elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+      add_definitions(-DLUA_DL_DLL=1)       # Enable loading of shared libraries (for Microsoft Windows)
+      
+    elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
+      add_definitions(-LUA_DL_DYLD=1)       # Enable loading of shared libraries (for Apple OS X)
+      
+    else()
+      message(FATAL_ERROR "Support your platform here")
+    endif()
+  endif()
 
   include_directories(
     ${LUA_SOURCES_DIR}/src
--- a/Resources/CMake/MongooseConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/MongooseConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -11,7 +11,7 @@
     # Use Mongoose 3.1
     DownloadPackage(
       "e718fc287b4eb1bd523be3fa00942bb0"
-      "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/mongoose-3.1.tgz"
+      "http://www.orthanc-server.com/downloads/third-party/mongoose-3.1.tgz"
       "${MONGOOSE_SOURCES_DIR}")
     
     add_definitions(-DMONGOOSE_USE_CALLBACKS=0)
@@ -21,7 +21,7 @@
     # Use Mongoose 3.8
     DownloadPackage(
       "7e3296295072792cdc3c633f9404e0c3"
-      "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/mongoose-3.8.tgz"
+      "http://www.orthanc-server.com/downloads/third-party/mongoose-3.8.tgz"
       "${MONGOOSE_SOURCES_DIR}")
     
     add_definitions(-DMONGOOSE_USE_CALLBACKS=1)
--- a/Resources/CMake/OpenSslConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/OpenSslConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,11 +1,7 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_OPENSSL)
-  # WARNING - We had to repack the upstream ".tar.gz" file to a ZIP
-  # file, as the upstream distribution ships symbolic links that are
-  # not always properly handled when uncompressing on Windows.
-
-  SET(OPENSSL_SOURCES_DIR ${CMAKE_BINARY_DIR}/openssl-1.0.2d)
-  SET(OPENSSL_URL "www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/openssl-1.0.2d.zip")
-  SET(OPENSSL_MD5 "4b2ac15fc6db17f3dadc54482d3eee85")
+  SET(OPENSSL_SOURCES_DIR ${CMAKE_BINARY_DIR}/openssl-1.0.2o)
+  SET(OPENSSL_URL "http://www.orthanc-server.com/downloads/third-party/openssl-1.0.2o.tar.gz")
+  SET(OPENSSL_MD5 "44279b8557c3247cbe324e2322ecd114")
 
   if (IS_DIRECTORY "${OPENSSL_SOURCES_DIR}")
     set(FirstRun OFF)
@@ -15,6 +11,116 @@
 
   DownloadPackage(${OPENSSL_MD5} ${OPENSSL_URL} "${OPENSSL_SOURCES_DIR}")
 
+  if (FirstRun)
+    file(MAKE_DIRECTORY ${OPENSSL_SOURCES_DIR}/include/openssl)
+
+    foreach(header
+      ${OPENSSL_SOURCES_DIR}/crypto/aes/aes.h
+      ${OPENSSL_SOURCES_DIR}/crypto/asn1/asn1.h
+      ${OPENSSL_SOURCES_DIR}/crypto/asn1/asn1_mac.h
+      ${OPENSSL_SOURCES_DIR}/crypto/asn1/asn1t.h
+      ${OPENSSL_SOURCES_DIR}/crypto/bf/blowfish.h
+      ${OPENSSL_SOURCES_DIR}/crypto/bio/bio.h
+      ${OPENSSL_SOURCES_DIR}/crypto/bn/bn.h
+      ${OPENSSL_SOURCES_DIR}/crypto/buffer/buffer.h
+      ${OPENSSL_SOURCES_DIR}/crypto/camellia/camellia.h
+      ${OPENSSL_SOURCES_DIR}/crypto/cast/cast.h
+      ${OPENSSL_SOURCES_DIR}/crypto/cmac/cmac.h
+      ${OPENSSL_SOURCES_DIR}/crypto/cms/cms.h
+      ${OPENSSL_SOURCES_DIR}/crypto/comp/comp.h
+      ${OPENSSL_SOURCES_DIR}/crypto/conf/conf.h
+      ${OPENSSL_SOURCES_DIR}/crypto/conf/conf_api.h
+      ${OPENSSL_SOURCES_DIR}/crypto/crypto.h
+      ${OPENSSL_SOURCES_DIR}/crypto/des/des.h
+      ${OPENSSL_SOURCES_DIR}/crypto/des/des_old.h
+      ${OPENSSL_SOURCES_DIR}/crypto/dh/dh.h
+      ${OPENSSL_SOURCES_DIR}/crypto/dsa/dsa.h
+      ${OPENSSL_SOURCES_DIR}/crypto/dso/dso.h
+      ${OPENSSL_SOURCES_DIR}/crypto/ebcdic.h
+      ${OPENSSL_SOURCES_DIR}/crypto/ec/ec.h
+      ${OPENSSL_SOURCES_DIR}/crypto/ecdh/ecdh.h
+      ${OPENSSL_SOURCES_DIR}/crypto/ecdsa/ecdsa.h
+      ${OPENSSL_SOURCES_DIR}/crypto/engine/engine.h
+      ${OPENSSL_SOURCES_DIR}/crypto/err/err.h
+      ${OPENSSL_SOURCES_DIR}/crypto/evp/evp.h
+      ${OPENSSL_SOURCES_DIR}/crypto/hmac/hmac.h
+      ${OPENSSL_SOURCES_DIR}/crypto/idea/idea.h
+      ${OPENSSL_SOURCES_DIR}/crypto/jpake/jpake.h
+      ${OPENSSL_SOURCES_DIR}/crypto/krb5/krb5_asn.h
+      ${OPENSSL_SOURCES_DIR}/crypto/lhash/lhash.h
+      ${OPENSSL_SOURCES_DIR}/crypto/md2/md2.h
+      ${OPENSSL_SOURCES_DIR}/crypto/md4/md4.h
+      ${OPENSSL_SOURCES_DIR}/crypto/md5/md5.h
+      ${OPENSSL_SOURCES_DIR}/crypto/mdc2/mdc2.h
+      ${OPENSSL_SOURCES_DIR}/crypto/modes/modes.h
+      ${OPENSSL_SOURCES_DIR}/crypto/objects/obj_mac.h
+      ${OPENSSL_SOURCES_DIR}/crypto/objects/objects.h
+      ${OPENSSL_SOURCES_DIR}/crypto/ocsp/ocsp.h
+      ${OPENSSL_SOURCES_DIR}/crypto/opensslconf.h
+      ${OPENSSL_SOURCES_DIR}/crypto/opensslv.h
+      ${OPENSSL_SOURCES_DIR}/crypto/ossl_typ.h
+      ${OPENSSL_SOURCES_DIR}/crypto/pem/pem.h
+      ${OPENSSL_SOURCES_DIR}/crypto/pem/pem2.h
+      ${OPENSSL_SOURCES_DIR}/crypto/pkcs12/pkcs12.h
+      ${OPENSSL_SOURCES_DIR}/crypto/pkcs7/pkcs7.h
+      ${OPENSSL_SOURCES_DIR}/crypto/pqueue/pqueue.h
+      ${OPENSSL_SOURCES_DIR}/crypto/rand/rand.h
+      ${OPENSSL_SOURCES_DIR}/crypto/rc2/rc2.h
+      ${OPENSSL_SOURCES_DIR}/crypto/rc4/rc4.h
+      ${OPENSSL_SOURCES_DIR}/crypto/rc5/rc5.h
+      ${OPENSSL_SOURCES_DIR}/crypto/ripemd/ripemd.h
+      ${OPENSSL_SOURCES_DIR}/crypto/rsa/rsa.h
+      ${OPENSSL_SOURCES_DIR}/crypto/seed/seed.h
+      ${OPENSSL_SOURCES_DIR}/crypto/sha/sha.h
+      ${OPENSSL_SOURCES_DIR}/crypto/srp/srp.h
+      ${OPENSSL_SOURCES_DIR}/crypto/stack/safestack.h
+      ${OPENSSL_SOURCES_DIR}/crypto/stack/stack.h
+      ${OPENSSL_SOURCES_DIR}/crypto/store/store.h
+      ${OPENSSL_SOURCES_DIR}/crypto/symhacks.h
+      ${OPENSSL_SOURCES_DIR}/crypto/ts/ts.h
+      ${OPENSSL_SOURCES_DIR}/crypto/txt_db/txt_db.h
+      ${OPENSSL_SOURCES_DIR}/crypto/ui/ui.h
+      ${OPENSSL_SOURCES_DIR}/crypto/ui/ui_compat.h
+      ${OPENSSL_SOURCES_DIR}/crypto/whrlpool/whrlpool.h
+      ${OPENSSL_SOURCES_DIR}/crypto/x509/x509.h
+      ${OPENSSL_SOURCES_DIR}/crypto/x509/x509_vfy.h
+      ${OPENSSL_SOURCES_DIR}/crypto/x509v3/x509v3.h
+      ${OPENSSL_SOURCES_DIR}/e_os2.h
+      ${OPENSSL_SOURCES_DIR}/ssl/dtls1.h
+      ${OPENSSL_SOURCES_DIR}/ssl/kssl.h
+      ${OPENSSL_SOURCES_DIR}/ssl/srtp.h
+      ${OPENSSL_SOURCES_DIR}/ssl/ssl.h
+      ${OPENSSL_SOURCES_DIR}/ssl/ssl2.h
+      ${OPENSSL_SOURCES_DIR}/ssl/ssl23.h
+      ${OPENSSL_SOURCES_DIR}/ssl/ssl3.h
+      ${OPENSSL_SOURCES_DIR}/ssl/tls1.h
+      )
+      file(COPY ${header} DESTINATION ${OPENSSL_SOURCES_DIR}/include/openssl)
+    endforeach()
+
+    file(RENAME
+      ${OPENSSL_SOURCES_DIR}/include/openssl/e_os2.h
+      ${OPENSSL_SOURCES_DIR}/include/openssl/e_os2_source.h)
+
+    # The following patch of "e_os2.h" prevents from building OpenSSL
+    # as a DLL under Windows. Otherwise, symbols have inconsistent
+    # linkage if ${OPENSSL_SOURCES} is used to create a DLL (notably
+    # if building an Orthanc plugin such as MySQL).
+    file(WRITE ${OPENSSL_SOURCES_DIR}/include/openssl/e_os2.h "
+#include \"e_os2_source.h\"
+#if defined(_WIN32)
+#  undef OPENSSL_EXPORT
+#  undef OPENSSL_IMPORT
+#  undef OPENSSL_EXTERN
+#  undef OPENSSL_GLOBAL
+#  define OPENSSL_EXPORT
+#  define OPENSSL_IMPORT
+#  define OPENSSL_EXTERN extern
+#  define OPENSSL_GLOBAL
+#endif
+")
+  endif()
+  
   add_definitions(
     -DOPENSSL_THREADS
     -DOPENSSL_IA32_SSE2
@@ -25,9 +131,6 @@
     -DOPENSSL_NO_BF 
     -DOPENSSL_NO_CAMELLIA
     -DOPENSSL_NO_CAST 
-    -DOPENSSL_NO_EC
-    -DOPENSSL_NO_ECDH
-    -DOPENSSL_NO_ECDSA
     -DOPENSSL_NO_EC_NISTP_64_GCC_128
     -DOPENSSL_NO_GMP
     -DOPENSSL_NO_GOST
@@ -37,7 +140,7 @@
     -DOPENSSL_NO_KRB5 
     -DOPENSSL_NO_MD2 
     -DOPENSSL_NO_MDC2 
-    -DOPENSSL_NO_MD4
+    #-DOPENSSL_NO_MD4   # MD4 is necessary for MariaDB/MySQL client
     -DOPENSSL_NO_RC2 
     -DOPENSSL_NO_RC4 
     -DOPENSSL_NO_RC5 
@@ -78,6 +181,7 @@
     ${OPENSSL_SOURCES_DIR}/crypto/evp
     ${OPENSSL_SOURCES_DIR}/crypto/hmac
     ${OPENSSL_SOURCES_DIR}/crypto/lhash
+    ${OPENSSL_SOURCES_DIR}/crypto/md4
     ${OPENSSL_SOURCES_DIR}/crypto/md5
     ${OPENSSL_SOURCES_DIR}/crypto/modes
     ${OPENSSL_SOURCES_DIR}/crypto/objects
@@ -99,6 +203,24 @@
     ${OPENSSL_SOURCES_DIR}/ssl
     )
 
+  if (ENABLE_OPENSSL_ENGINES)
+    list(APPEND OPENSSL_SOURCES_SUBDIRS
+      ${OPENSSL_SOURCES_DIR}/engines
+      )
+  endif()
+  
+  list(APPEND OPENSSL_SOURCES_SUBDIRS
+    # EC, ECDH and ECDSA are necessary for PKCS11, and for contacting
+    # HTTPS servers that use TLS certificate encrypted with ECDSA
+    # (check the output of a recent version of the "sslscan"
+    # command). Until Orthanc <= 1.4.1, these features were only
+    # enabled if ENABLE_PKCS11 support was set to "ON".
+    # https://groups.google.com/d/msg/orthanc-users/2l-bhYIMEWg/oMmK33bYBgAJ
+    ${OPENSSL_SOURCES_DIR}/crypto/ec
+    ${OPENSSL_SOURCES_DIR}/crypto/ecdh
+    ${OPENSSL_SOURCES_DIR}/crypto/ecdsa
+    )
+
   foreach(d ${OPENSSL_SOURCES_SUBDIRS})
     AUX_SOURCE_DIRECTORY(${d} OPENSSL_SOURCES)
   endforeach()
@@ -124,6 +246,9 @@
     ${OPENSSL_SOURCES_DIR}/crypto/evp/e_dsa.c
     ${OPENSSL_SOURCES_DIR}/crypto/evp/m_ripemd.c
     ${OPENSSL_SOURCES_DIR}/crypto/lhash/lh_test.c
+    ${OPENSSL_SOURCES_DIR}/crypto/md4/md4.c
+    ${OPENSSL_SOURCES_DIR}/crypto/md4/md4s.cpp
+    ${OPENSSL_SOURCES_DIR}/crypto/md4/md4test.c
     ${OPENSSL_SOURCES_DIR}/crypto/md5/md5s.cpp
     ${OPENSSL_SOURCES_DIR}/crypto/pkcs7/bio_ber.c
     ${OPENSSL_SOURCES_DIR}/crypto/pkcs7/pk7_enc.c
@@ -173,26 +298,42 @@
     ${OPENSSL_SOURCES_DIR}/crypto/dh/dhtest.c
     ${OPENSSL_SOURCES_DIR}/crypto/pqueue/pq_test.c
     ${OPENSSL_SOURCES_DIR}/crypto/des/ncbc_enc.c
+
+    ${OPENSSL_SOURCES_DIR}/crypto/evp/evp_extra_test.c
+    ${OPENSSL_SOURCES_DIR}/crypto/evp/verify_extra_test.c
+    ${OPENSSL_SOURCES_DIR}/crypto/x509/verify_extra_test.c
+    ${OPENSSL_SOURCES_DIR}/crypto/x509v3/v3prin.c
+    ${OPENSSL_SOURCES_DIR}/crypto/x509v3/v3nametest.c
+    ${OPENSSL_SOURCES_DIR}/crypto/constant_time_test.c
+
+    ${OPENSSL_SOURCES_DIR}/ssl/heartbeat_test.c
+    ${OPENSSL_SOURCES_DIR}/ssl/fatalerrtest.c
+    ${OPENSSL_SOURCES_DIR}/ssl/dtlstest.c
+    ${OPENSSL_SOURCES_DIR}/ssl/bad_dtls_test.c
+    ${OPENSSL_SOURCES_DIR}/ssl/clienthellotest.c
+    ${OPENSSL_SOURCES_DIR}/ssl/sslv2conftest.c
+
+    ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistz256.c
+    ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistz256_table.c
+    ${OPENSSL_SOURCES_DIR}/crypto/ec/ectest.c
+    ${OPENSSL_SOURCES_DIR}/crypto/ecdh/ecdhtest.c
+    ${OPENSSL_SOURCES_DIR}/crypto/ecdsa/ecdsatest.c
     )
 
+
   if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
     set_source_files_properties(
       ${OPENSSL_SOURCES}
       PROPERTIES COMPILE_DEFINITIONS
       "OPENSSL_SYSNAME_WIN32;SO_WIN32;WIN32_LEAN_AND_MEAN;L_ENDIAN")
 
-  elseif ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-    execute_process(
-      COMMAND ${PATCH_EXECUTABLE} -N ui_openssl.c -i ${ORTHANC_ROOT}/Resources/Patches/openssl-lsb.diff
-      WORKING_DIRECTORY ${OPENSSL_SOURCES_DIR}/crypto/ui
-      RESULT_VARIABLE Failure
-      )
-
-    if (Failure AND FirstRun)
-      message(FATAL_ERROR "Error while patching a file")
+    if (ENABLE_OPENSSL_ENGINES)
+      link_libraries(crypt32)
     endif()
   endif()
 
+  source_group(ThirdParty\\OpenSSL REGULAR_EXPRESSION ${OPENSSL_SOURCES_DIR}/.*)
+
 else()
   include(FindOpenSSL)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,620 @@
+##
+## This is a CMake configuration file that configures the core
+## libraries of Orthanc. This file can be used by external projects so
+## as to gain access to the Orthanc APIs (the most prominent examples
+## are currently "Stone of Orthanc" and "Orthanc for whole-slide
+## imaging plugin").
+##
+
+
+#####################################################################
+## Configuration of the components
+#####################################################################
+
+# Path to the root folder of the Orthanc distribution
+set(ORTHANC_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
+
+# Some basic inclusions
+include(CMakePushCheckState)
+include(CheckFunctionExists)
+include(CheckIncludeFile)
+include(CheckIncludeFileCXX)
+include(CheckIncludeFiles)
+include(CheckLibraryExists)
+include(CheckStructHasMember)
+include(CheckSymbolExists)
+include(CheckTypeSize)
+include(FindPythonInterp)
+  
+include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake)
+include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake)
+include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake)
+
+
+#####################################################################
+## Disable unneeded macros
+#####################################################################
+
+if (NOT ENABLE_SQLITE)
+  unset(USE_SYSTEM_SQLITE CACHE)
+  add_definitions(-DORTHANC_ENABLE_SQLITE=0)
+endif()
+
+if (NOT ENABLE_CRYPTO_OPTIONS)
+  unset(ENABLE_SSL CACHE)
+  unset(ENABLE_PKCS11 CACHE)
+  unset(ENABLE_OPENSSL_ENGINES CACHE)
+  unset(USE_SYSTEM_OPENSSL CACHE)
+  unset(USE_SYSTEM_LIBP11 CACHE)
+  add_definitions(
+    -DORTHANC_ENABLE_SSL=0
+    -DORTHANC_ENABLE_PKCS11=0
+    )
+endif()
+
+if (NOT ENABLE_WEB_CLIENT)
+  unset(USE_SYSTEM_CURL CACHE)
+  add_definitions(-DORTHANC_ENABLE_CURL=0)
+endif()
+
+if (NOT ENABLE_WEB_SERVER)
+  unset(ENABLE_CIVETWEB CACHE)
+  unset(USE_SYSTEM_CIVETWEB CACHE)
+  unset(USE_SYSTEM_MONGOOSE CACHE)
+  add_definitions(
+    -DORTHANC_ENABLE_CIVETWEB=0
+    -DORTHANC_ENABLE_MONGOOSE=0
+    )
+endif()
+
+if (NOT ENABLE_JPEG)
+  unset(USE_SYSTEM_LIBJPEG CACHE)
+  add_definitions(-DORTHANC_ENABLE_JPEG=0)
+endif()
+
+if (NOT ENABLE_ZLIB)
+  unset(USE_SYSTEM_ZLIB CACHE)
+  add_definitions(-DORTHANC_ENABLE_ZLIB=0)
+endif()
+
+if (NOT ENABLE_PNG)
+  unset(USE_SYSTEM_LIBPNG CACHE)
+  add_definitions(-DORTHANC_ENABLE_PNG=0)
+endif()
+
+if (NOT ENABLE_LUA)
+  unset(USE_SYSTEM_LUA CACHE)
+  unset(ENABLE_LUA_MODULES CACHE)
+  add_definitions(-DORTHANC_ENABLE_LUA=0)
+endif()
+
+if (NOT ENABLE_PUGIXML)
+  unset(USE_SYSTEM_PUGIXML CACHE)
+  add_definitions(-DORTHANC_ENABLE_PUGIXML=0)
+endif()
+
+if (NOT ENABLE_LOCALE)
+  unset(USE_SYSTEM_LIBICONV CACHE)
+  add_definitions(-DORTHANC_ENABLE_LOCALE=0)
+endif()
+
+if (NOT ENABLE_GOOGLE_TEST)
+  unset(USE_SYSTEM_GOOGLE_TEST CACHE)
+  unset(USE_GOOGLE_TEST_DEBIAN_PACKAGE CACHE)
+endif()
+
+if (NOT ENABLE_DCMTK)
+  add_definitions(
+    -DORTHANC_ENABLE_DCMTK=0
+    -DORTHANC_ENABLE_DCMTK_NETWORKING=0
+    )
+  unset(DCMTK_DICTIONARY_DIR CACHE)
+  unset(USE_DCMTK_360 CACHE)
+  unset(USE_DCMTK_362_PRIVATE_DIC CACHE)
+  unset(USE_SYSTEM_DCMTK CACHE)
+  unset(ENABLE_DCMTK_JPEG CACHE)
+  unset(ENABLE_DCMTK_JPEG_LOSSLESS CACHE)
+endif()
+
+
+#####################################################################
+## List of source files
+#####################################################################
+
+set(ORTHANC_CORE_SOURCES_INTERNAL
+  ${ORTHANC_ROOT}/Core/Cache/MemoryCache.cpp
+  ${ORTHANC_ROOT}/Core/ChunkedBuffer.cpp
+  ${ORTHANC_ROOT}/Core/DicomFormat/DicomTag.cpp
+  ${ORTHANC_ROOT}/Core/Enumerations.cpp
+  ${ORTHANC_ROOT}/Core/FileStorage/MemoryStorageArea.cpp
+  ${ORTHANC_ROOT}/Core/Logging.cpp
+  ${ORTHANC_ROOT}/Core/SerializationToolbox.cpp
+  ${ORTHANC_ROOT}/Core/Toolbox.cpp
+  ${ORTHANC_ROOT}/Core/WebServiceParameters.cpp
+  )
+
+if (ENABLE_MODULE_IMAGES)
+  list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+    ${ORTHANC_ROOT}/Core/Images/Font.cpp
+    ${ORTHANC_ROOT}/Core/Images/FontRegistry.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/PamReader.cpp
+    ${ORTHANC_ROOT}/Core/Images/PamWriter.cpp
+    )
+endif()
+
+if (ENABLE_MODULE_DICOM)
+  list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+    ${ORTHANC_ROOT}/Core/DicomFormat/DicomArray.cpp
+    ${ORTHANC_ROOT}/Core/DicomFormat/DicomImageInformation.cpp
+    ${ORTHANC_ROOT}/Core/DicomFormat/DicomInstanceHasher.cpp
+    ${ORTHANC_ROOT}/Core/DicomFormat/DicomIntegerPixelAccessor.cpp
+    ${ORTHANC_ROOT}/Core/DicomFormat/DicomMap.cpp
+    ${ORTHANC_ROOT}/Core/DicomFormat/DicomValue.cpp
+    )
+endif()
+
+if (ENABLE_MODULE_JOBS)
+  list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+    ${ORTHANC_ROOT}/Core/JobsEngine/GenericJobUnserializer.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/JobInfo.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/JobStatus.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/JobStepResult.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/Operations/JobOperationValues.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/Operations/LogJobOperation.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/SetOfCommandsJob.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/SetOfInstancesJob.cpp
+    )
+endif()
+
+
+
+#####################################################################
+## Configuration of optional third-party dependencies
+#####################################################################
+
+
+##
+## Embedded database: SQLite
+##
+
+if (ENABLE_SQLITE)
+  include(${CMAKE_CURRENT_LIST_DIR}/SQLiteConfiguration.cmake)
+  add_definitions(-DORTHANC_ENABLE_SQLITE=1)
+
+  list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+    ${ORTHANC_ROOT}/Core/SQLite/Connection.cpp
+    ${ORTHANC_ROOT}/Core/SQLite/FunctionContext.cpp
+    ${ORTHANC_ROOT}/Core/SQLite/Statement.cpp
+    ${ORTHANC_ROOT}/Core/SQLite/StatementId.cpp
+    ${ORTHANC_ROOT}/Core/SQLite/StatementReference.cpp
+    ${ORTHANC_ROOT}/Core/SQLite/Transaction.cpp
+    )
+endif()
+
+
+##
+## Cryptography: OpenSSL and libp11
+## Must be above "ENABLE_WEB_CLIENT" and "ENABLE_WEB_SERVER"
+##
+
+if (ENABLE_CRYPTO_OPTIONS)
+  if (ENABLE_SSL)
+    include(${CMAKE_CURRENT_LIST_DIR}/OpenSslConfiguration.cmake)
+    add_definitions(-DORTHANC_ENABLE_SSL=1)
+  else()
+    unset(ENABLE_OPENSSL_ENGINES CACHE)
+    unset(USE_SYSTEM_OPENSSL CACHE)
+    add_definitions(-DORTHANC_ENABLE_SSL=0)
+  endif()
+
+  if (ENABLE_PKCS11)
+    if (ENABLE_SSL)
+      include(${CMAKE_CURRENT_LIST_DIR}/LibP11Configuration.cmake)
+
+      add_definitions(-DORTHANC_ENABLE_PKCS11=1)
+      list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+        ${ORTHANC_ROOT}/Core/Pkcs11.cpp
+        )
+    else()
+      message(FATAL_ERROR "OpenSSL is required to enable PKCS#11 support")
+    endif()
+  else()
+    add_definitions(-DORTHANC_ENABLE_PKCS11=0)  
+  endif()
+endif()
+
+
+##
+## HTTP client: libcurl
+##
+
+if (ENABLE_WEB_CLIENT)
+  include(${CMAKE_CURRENT_LIST_DIR}/LibCurlConfiguration.cmake)
+  add_definitions(-DORTHANC_ENABLE_CURL=1)
+
+  list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+    ${ORTHANC_ROOT}/Core/HttpClient.cpp
+    )
+endif()
+
+
+##
+## HTTP server: Mongoose 3.8 or Civetweb
+##
+
+if (ENABLE_WEB_SERVER)
+  if (ENABLE_CIVETWEB)
+    include(${CMAKE_CURRENT_LIST_DIR}/CivetwebConfiguration.cmake)
+    add_definitions(
+      -DORTHANC_ENABLE_CIVETWEB=1
+      -DORTHANC_ENABLE_MONGOOSE=0
+      )
+  else()
+    include(${CMAKE_CURRENT_LIST_DIR}/MongooseConfiguration.cmake)
+    add_definitions(
+      -DORTHANC_ENABLE_CIVETWEB=0
+      -DORTHANC_ENABLE_MONGOOSE=1
+      )
+  endif()
+
+  list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+    ${ORTHANC_ROOT}/Core/HttpServer/BufferHttpSender.cpp
+    ${ORTHANC_ROOT}/Core/HttpServer/FilesystemHttpHandler.cpp
+    ${ORTHANC_ROOT}/Core/HttpServer/FilesystemHttpSender.cpp
+    ${ORTHANC_ROOT}/Core/HttpServer/HttpContentNegociation.cpp
+    ${ORTHANC_ROOT}/Core/HttpServer/HttpFileSender.cpp
+    ${ORTHANC_ROOT}/Core/HttpServer/HttpOutput.cpp
+    ${ORTHANC_ROOT}/Core/HttpServer/HttpStreamTranscoder.cpp
+    ${ORTHANC_ROOT}/Core/HttpServer/HttpToolbox.cpp
+    ${ORTHANC_ROOT}/Core/HttpServer/MongooseServer.cpp
+    ${ORTHANC_ROOT}/Core/HttpServer/StringHttpOutput.cpp
+    ${ORTHANC_ROOT}/Core/RestApi/RestApi.cpp
+    ${ORTHANC_ROOT}/Core/RestApi/RestApiCall.cpp
+    ${ORTHANC_ROOT}/Core/RestApi/RestApiGetCall.cpp
+    ${ORTHANC_ROOT}/Core/RestApi/RestApiHierarchy.cpp
+    ${ORTHANC_ROOT}/Core/RestApi/RestApiOutput.cpp
+    ${ORTHANC_ROOT}/Core/RestApi/RestApiPath.cpp
+    )
+endif()
+
+
+##
+## JPEG support: libjpeg
+##
+
+if (ENABLE_JPEG)
+  if (NOT ENABLE_MODULE_IMAGES)
+    message(FATAL_ERROR "Image processing primitives must be enabled if enabling libjpeg support")
+  endif()
+
+  include(${CMAKE_CURRENT_LIST_DIR}/LibJpegConfiguration.cmake)
+  add_definitions(-DORTHANC_ENABLE_JPEG=1)
+
+  list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+    ${ORTHANC_ROOT}/Core/Images/JpegErrorManager.cpp
+    ${ORTHANC_ROOT}/Core/Images/JpegReader.cpp
+    ${ORTHANC_ROOT}/Core/Images/JpegWriter.cpp
+    )
+endif()
+
+
+##
+## zlib support
+##
+
+if (ENABLE_ZLIB)
+  include(${CMAKE_CURRENT_LIST_DIR}/ZlibConfiguration.cmake)
+  add_definitions(-DORTHANC_ENABLE_ZLIB=1)
+
+  list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+    ${ORTHANC_ROOT}/Core/Compression/DeflateBaseCompressor.cpp
+    ${ORTHANC_ROOT}/Core/Compression/GzipCompressor.cpp
+    ${ORTHANC_ROOT}/Core/Compression/ZlibCompressor.cpp
+    )
+
+  if (NOT ORTHANC_SANDBOXED)
+    list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+      ${ORTHANC_ROOT}/Core/Compression/HierarchicalZipWriter.cpp
+      ${ORTHANC_ROOT}/Core/Compression/ZipWriter.cpp
+      ${ORTHANC_ROOT}/Core/FileStorage/StorageAccessor.cpp
+      )
+  endif()
+endif()
+
+
+##
+## PNG support: libpng (in conjunction with zlib)
+##
+
+if (ENABLE_PNG)
+  if (NOT ENABLE_ZLIB)
+    message(FATAL_ERROR "Support for zlib must be enabled if enabling libpng support")
+  endif()
+
+  if (NOT ENABLE_MODULE_IMAGES)
+    message(FATAL_ERROR "Image processing primitives must be enabled if enabling libpng support")
+  endif()
+  
+  include(${CMAKE_CURRENT_LIST_DIR}/LibPngConfiguration.cmake)
+  add_definitions(-DORTHANC_ENABLE_PNG=1)
+
+  list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+    ${ORTHANC_ROOT}/Core/Images/PngReader.cpp
+    ${ORTHANC_ROOT}/Core/Images/PngWriter.cpp
+    )
+endif()
+
+
+##
+## Lua support
+##
+
+if (ENABLE_LUA)
+  include(${CMAKE_CURRENT_LIST_DIR}/LuaConfiguration.cmake)
+  add_definitions(-DORTHANC_ENABLE_LUA=1)
+
+  list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+    ${ORTHANC_ROOT}/Core/Lua/LuaContext.cpp
+    ${ORTHANC_ROOT}/Core/Lua/LuaFunctionCall.cpp
+    )
+endif()
+
+
+##
+## XML support: pugixml
+##
+
+if (ENABLE_PUGIXML)
+  include(${CMAKE_CURRENT_LIST_DIR}/PugixmlConfiguration.cmake)
+  add_definitions(-DORTHANC_ENABLE_PUGIXML=1)
+endif()
+
+
+##
+## Locale support: libiconv
+##
+
+if (ENABLE_LOCALE)
+  if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+    # In WebAssembly or asm.js, we rely on the version of iconv that
+    # is shipped with the stdlib
+    unset(USE_BOOST_ICONV CACHE)
+  else()
+    include(${CMAKE_CURRENT_LIST_DIR}/LibIconvConfiguration.cmake)
+  endif()
+  
+  add_definitions(-DORTHANC_ENABLE_LOCALE=1)
+endif()
+
+
+##
+## Google Test for unit testing
+##
+
+if (ENABLE_GOOGLE_TEST)
+  include(${CMAKE_CURRENT_LIST_DIR}/GoogleTestConfiguration.cmake)
+endif()
+
+
+
+#####################################################################
+## Inclusion of mandatory third-party dependencies
+#####################################################################
+
+include(${CMAKE_CURRENT_LIST_DIR}/JsonCppConfiguration.cmake)
+include(${CMAKE_CURRENT_LIST_DIR}/UuidConfiguration.cmake)
+
+# We put Boost as the last dependency, as it is the heaviest to
+# configure, which allows to quickly spot problems when configuring
+# static builds in other dependencies
+include(${CMAKE_CURRENT_LIST_DIR}/BoostConfiguration.cmake)
+
+
+#####################################################################
+## Optional configuration of DCMTK
+#####################################################################
+
+if (ENABLE_DCMTK)
+  if (NOT ENABLE_LOCALE)
+    message(FATAL_ERROR "Support for locales must be enabled if enabling DCMTK support")
+  endif()
+
+  if (NOT ENABLE_MODULE_DICOM)
+    message(FATAL_ERROR "DICOM module must be enabled if enabling DCMTK support")
+  endif()
+
+  include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfiguration.cmake)
+
+  add_definitions(-DORTHANC_ENABLE_DCMTK=1)
+
+  if (ENABLE_DCMTK_JPEG)
+    add_definitions(-DORTHANC_ENABLE_DCMTK_JPEG=1)
+  else()
+    add_definitions(-DORTHANC_ENABLE_DCMTK_JPEG=0)
+  endif()
+
+  if (ENABLE_DCMTK_JPEG_LOSSLESS)
+    add_definitions(-DORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS=1)
+  else()
+    add_definitions(-DORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS=0)
+  endif()
+
+  set(ORTHANC_DICOM_SOURCES_INTERNAL
+    ${ORTHANC_ROOT}/Core/DicomParsing/DicomModification.cpp
+    ${ORTHANC_ROOT}/Core/DicomParsing/FromDcmtkBridge.cpp
+    ${ORTHANC_ROOT}/Core/DicomParsing/ParsedDicomFile.cpp
+    ${ORTHANC_ROOT}/Core/DicomParsing/ToDcmtkBridge.cpp
+
+    ${ORTHANC_ROOT}/Core/DicomParsing/Internals/DicomFrameIndex.cpp
+    ${ORTHANC_ROOT}/Core/DicomParsing/Internals/DicomImageDecoder.cpp
+    )
+
+  if (NOT ORTHANC_SANDBOXED)
+    list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+      ${ORTHANC_ROOT}/Core/DicomParsing/DicomDirWriter.cpp
+      )
+  endif()
+
+  if (ENABLE_DCMTK_NETWORKING)
+    add_definitions(-DORTHANC_ENABLE_DCMTK_NETWORKING=1)
+    list(APPEND ORTHANC_DICOM_SOURCES_INTERNAL
+      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomFindAnswers.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomServer.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomUserConnection.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/CommandDispatcher.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/FindScp.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/MoveScp.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/StoreScp.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/RemoteModalityParameters.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp
+      )
+  else()
+    add_definitions(-DORTHANC_ENABLE_DCMTK_NETWORKING=0)
+  endif()
+
+  if (STANDALONE_BUILD AND NOT HAS_EMBEDDED_RESOURCES)
+    EmbedResources(
+      ${DCMTK_DICTIONARIES}
+      )
+    list(APPEND ORTHANC_DICOM_SOURCES_DEPENDENCIES
+      ${AUTOGENERATED_SOURCES}
+      )
+  endif()
+endif()
+
+
+#####################################################################
+## Configuration of the C/C++ macros
+#####################################################################
+
+add_definitions(
+  -DORTHANC_API_VERSION="${ORTHANC_API_VERSION}"
+  -DORTHANC_DATABASE_VERSION=${ORTHANC_DATABASE_VERSION}
+  -DORTHANC_DEFAULT_DICOM_ENCODING=Encoding_Latin1
+  -DORTHANC_ENABLE_BASE64=1
+  -DORTHANC_ENABLE_MD5=1
+  -DORTHANC_MAXIMUM_TAG_LENGTH=256
+  -DORTHANC_VERSION="${ORTHANC_VERSION}"
+  )
+
+
+if (ORTHANC_SANDBOXED)
+  add_definitions(
+    -DORTHANC_SANDBOXED=1
+    -DORTHANC_ENABLE_LOGGING_PLUGIN=0
+    )
+
+  if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+    add_definitions(
+      -DORTHANC_ENABLE_LOGGING=1
+      -DORTHANC_ENABLE_LOGGING_STDIO=1
+      )
+  else()
+    add_definitions(
+      -DORTHANC_ENABLE_LOGGING=0
+      )
+  endif()
+  
+else()
+  add_definitions(
+    -DORTHANC_ENABLE_LOGGING=1
+    -DORTHANC_ENABLE_LOGGING_STDIO=0
+    -DORTHANC_SANDBOXED=0
+    )
+
+  list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+    ${ORTHANC_ROOT}/Core/Cache/SharedArchive.cpp
+    ${ORTHANC_ROOT}/Core/FileStorage/FilesystemStorage.cpp
+    ${ORTHANC_ROOT}/Core/MultiThreading/RunnableWorkersPool.cpp
+    ${ORTHANC_ROOT}/Core/MultiThreading/Semaphore.cpp
+    ${ORTHANC_ROOT}/Core/MultiThreading/SharedMessageQueue.cpp
+    ${ORTHANC_ROOT}/Core/SharedLibrary.cpp
+    ${ORTHANC_ROOT}/Core/SystemToolbox.cpp
+    ${ORTHANC_ROOT}/Core/TemporaryFile.cpp
+    )
+
+  if (ENABLE_MODULE_JOBS)
+    list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+      ${ORTHANC_ROOT}/Core/JobsEngine/JobsEngine.cpp
+      ${ORTHANC_ROOT}/Core/JobsEngine/JobsRegistry.cpp
+      )
+  endif()
+endif()
+
+
+if (HAS_EMBEDDED_RESOURCES)
+  add_definitions(-DORTHANC_HAS_EMBEDDED_RESOURCES=1)
+
+  if (ENABLE_WEB_SERVER)
+    list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+      ${ORTHANC_ROOT}/Core/HttpServer/EmbeddedResourceHttpHandler.cpp
+      )
+  endif()
+else()
+  add_definitions(-DORTHANC_HAS_EMBEDDED_RESOURCES=0)
+endif()
+
+
+#####################################################################
+## Gathering of all the source code
+#####################################################################
+
+# The "xxx_INTERNAL" variables list the source code that belongs to
+# the Orthanc project. It can be used to configure precompiled headers
+# if using Microsoft Visual Studio.
+
+# The "xxx_DEPENDENCIES" variables list the source code coming from
+# third-party dependencies.
+
+
+set(ORTHANC_CORE_SOURCES_DEPENDENCIES
+  ${BOOST_SOURCES}
+  ${CIVETWEB_SOURCES}
+  ${CURL_SOURCES}
+  ${JSONCPP_SOURCES}
+  ${LIBICONV_SOURCES}
+  ${LIBJPEG_SOURCES}
+  ${LIBP11_SOURCES}
+  ${LIBPNG_SOURCES}
+  ${LUA_SOURCES}
+  ${MONGOOSE_SOURCES}
+  ${OPENSSL_SOURCES}
+  ${PUGIXML_SOURCES}
+  ${SQLITE_SOURCES}
+  ${UUID_SOURCES}
+  ${ZLIB_SOURCES}
+
+  ${ORTHANC_ROOT}/Resources/ThirdParty/md5/md5.c
+  ${ORTHANC_ROOT}/Resources/ThirdParty/base64/base64.cpp
+  )
+
+if (ENABLE_ZLIB AND NOT ORTHANC_SANDBOXED)
+  list(APPEND ORTHANC_CORE_SOURCES_DEPENDENCIES
+    # This is the minizip distribution to create ZIP files using zlib
+    ${ORTHANC_ROOT}/Resources/ThirdParty/minizip/ioapi.c
+    ${ORTHANC_ROOT}/Resources/ThirdParty/minizip/zip.c
+    )
+endif()
+
+
+set(ORTHANC_CORE_SOURCES
+  ${ORTHANC_CORE_SOURCES_INTERNAL}
+  ${ORTHANC_CORE_SOURCES_DEPENDENCIES}
+  )
+
+if (ENABLE_DCMTK)
+  list(APPEND ORTHANC_DICOM_SOURCES_DEPENDENCIES
+    ${DCMTK_SOURCES}
+    )
+  
+  set(ORTHANC_DICOM_SOURCES
+    ${ORTHANC_DICOM_SOURCES_INTERNAL}
+    ${ORTHANC_DICOM_SOURCES_DEPENDENCIES}
+    )
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/OrthancFrameworkParameters.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,118 @@
+#####################################################################
+## Versioning information
+#####################################################################
+
+# Version of the build, should always be "mainline" except in release branches
+set(ORTHANC_VERSION "mainline")
+
+# Version of the database schema. History:
+#   * Orthanc 0.1.0 -> Orthanc 0.3.0 = no versioning
+#   * Orthanc 0.3.1                  = version 2
+#   * Orthanc 0.4.0 -> Orthanc 0.7.2 = version 3
+#   * Orthanc 0.7.3 -> Orthanc 0.8.4 = version 4
+#   * Orthanc 0.8.5 -> Orthanc 0.9.4 = version 5
+#   * Orthanc 0.9.5 -> mainline      = version 6
+set(ORTHANC_DATABASE_VERSION 6)
+
+# Version of the Orthanc API, can be retrieved from "/system" URI in
+# order to check whether new URI endpoints are available even if using
+# the mainline version of Orthanc
+set(ORTHANC_API_VERSION "1.2")
+
+
+#####################################################################
+## CMake parameters tunable by the user
+#####################################################################
+
+# Support of static compilation
+set(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages")
+set(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
+set(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)")
+
+# Generic parameters of the build
+set(ENABLE_CIVETWEB OFF CACHE BOOL "Use Civetweb instead of Mongoose (experimental)")
+set(ENABLE_PKCS11 OFF CACHE BOOL "Enable PKCS#11 for HTTPS client authentication using hardware security modules and smart cards")
+set(ENABLE_PROFILING OFF CACHE BOOL "Whether to enable the generation of profiling information with gprof")
+set(ENABLE_SSL ON CACHE BOOL "Include support for SSL")
+set(ENABLE_LUA_MODULES OFF CACHE BOOL "Enable support for loading external Lua modules (only meaningful if using static version of the Lua engine)")
+
+# Parameters to fine-tune linking against system libraries
+set(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost")
+set(USE_SYSTEM_CIVETWEB ON CACHE BOOL "Use the system version of Civetweb (experimental)")
+set(USE_SYSTEM_CURL ON CACHE BOOL "Use the system version of LibCurl")
+set(USE_SYSTEM_GOOGLE_TEST ON CACHE BOOL "Use the system version of Google Test")
+set(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp")
+set(USE_SYSTEM_LIBICONV ON CACHE BOOL "Use the system version of libiconv")
+set(USE_SYSTEM_LIBJPEG ON CACHE BOOL "Use the system version of libjpeg")
+set(USE_SYSTEM_LIBP11 OFF CACHE BOOL "Use the system version of libp11 (PKCS#11 wrapper library)")
+set(USE_SYSTEM_LIBPNG ON CACHE BOOL "Use the system version of libpng")
+set(USE_SYSTEM_LUA ON CACHE BOOL "Use the system version of Lua")
+set(USE_SYSTEM_MONGOOSE ON CACHE BOOL "Use the system version of Mongoose")
+set(USE_SYSTEM_OPENSSL ON CACHE BOOL "Use the system version of OpenSSL")
+set(USE_SYSTEM_PUGIXML ON CACHE BOOL "Use the system version of Pugixml")
+set(USE_SYSTEM_SQLITE ON CACHE BOOL "Use the system version of SQLite")
+set(USE_SYSTEM_UUID ON CACHE BOOL "Use the system version of the uuid library from e2fsprogs")
+set(USE_SYSTEM_ZLIB ON CACHE BOOL "Use the system version of ZLib")
+
+# Parameters specific to DCMTK
+set(DCMTK_DICTIONARY_DIR "" CACHE PATH "Directory containing the DCMTK dictionaries \"dicom.dic\" and \"private.dic\" (only when using system version of DCMTK)") 
+set(USE_DCMTK_360 OFF CACHE BOOL "Use older DCMTK version 3.6.0 in static builds (instead of default 3.6.2)")
+set(USE_DCMTK_362_PRIVATE_DIC ON CACHE BOOL "Use the dictionary of private tags from DCMTK 3.6.2 if using DCMTK 3.6.0")
+set(USE_SYSTEM_DCMTK ON CACHE BOOL "Use the system version of DCMTK")
+set(ENABLE_DCMTK_JPEG ON CACHE BOOL "Enable JPEG-LS (Lossless) decompression")
+set(ENABLE_DCMTK_JPEG_LOSSLESS ON CACHE BOOL "Enable JPEG-LS (Lossless) decompression")
+
+# Advanced and distribution-specific parameters
+set(USE_GOOGLE_TEST_DEBIAN_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)")
+set(SYSTEM_MONGOOSE_USE_CALLBACKS ON CACHE BOOL "The system version of Mongoose uses callbacks (version >= 3.7)")
+set(USE_BOOST_ICONV ON CACHE BOOL "Use iconv instead of wconv (Windows only)")
+set(USE_PUGIXML ON CACHE BOOL "Use the Pugixml parser (turn off only for debug)")
+set(USE_LEGACY_JSONCPP OFF CACHE BOOL "Use the old branch 0.x.y of JsonCpp, that does not require a C++11 compiler (for old versions of Visual Studio)")
+
+mark_as_advanced(USE_GOOGLE_TEST_DEBIAN_PACKAGE)
+mark_as_advanced(SYSTEM_MONGOOSE_USE_CALLBACKS)
+mark_as_advanced(USE_BOOST_ICONV)
+mark_as_advanced(USE_PUGIXML)
+mark_as_advanced(USE_LEGACY_JSONCPP)
+
+
+#####################################################################
+## Internal CMake parameters to enable the optional subcomponents of
+## the Orthanc framework
+#####################################################################
+
+# These options must be set to "ON" if compiling Orthanc, but might be
+# set to "OFF" by third-party projects if their associated features
+# are not required
+
+set(ENABLE_CRYPTO_OPTIONS OFF CACHE INTERNAL "Show options related to cryptography")
+set(ENABLE_JPEG OFF CACHE INTERNAL "Enable support of JPEG")
+set(ENABLE_GOOGLE_TEST OFF CACHE INTERNAL "Enable support of Google Test")
+set(ENABLE_LOCALE OFF CACHE INTERNAL "Enable support for locales (notably in Boost)")
+set(ENABLE_LUA OFF CACHE INTERNAL "Enable support of Lua scripting")
+set(ENABLE_PNG OFF CACHE INTERNAL "Enable support of PNG")
+set(ENABLE_PUGIXML OFF CACHE INTERNAL "Enable support of XML through Pugixml")
+set(ENABLE_SQLITE OFF CACHE INTERNAL "Enable support of SQLite databases")
+set(ENABLE_ZLIB OFF CACHE INTERNAL "Enable support of zlib")
+set(ENABLE_WEB_CLIENT OFF CACHE INTERNAL "Enable Web client")
+set(ENABLE_WEB_SERVER OFF CACHE INTERNAL "Enable embedded Web server")
+set(ENABLE_DCMTK OFF CACHE INTERNAL "Enable DCMTK")
+set(ENABLE_DCMTK_NETWORKING OFF CACHE INTERNAL "Enable DICOM networking in DCMTK")
+set(ENABLE_OPENSSL_ENGINES OFF CACHE INTERNAL "Enable support of engines in OpenSSL")
+
+set(HAS_EMBEDDED_RESOURCES OFF CACHE INTERNAL
+  "Whether resources are auto-generated using EmbedResources.py")
+
+set(ORTHANC_SANDBOXED OFF CACHE INTERNAL
+  "Whether Orthanc runs inside a sandboxed environment (such as Google NaCl or WebAssembly)")
+
+
+#
+# These options can be used to turn off some modules of the Orthanc
+# framework, in order to speed up the compilation time of third-party
+# projects.
+#
+
+set(ENABLE_MODULE_IMAGES ON CACHE INTERNAL "Enable module for image processing")
+set(ENABLE_MODULE_JOBS ON CACHE INTERNAL "Enable module for jobs")
+set(ENABLE_MODULE_DICOM ON CACHE INTERNAL "Enable module for DICOM handling")
--- a/Resources/CMake/PugixmlConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/PugixmlConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,31 +1,26 @@
-if (USE_PUGIXML)
-  add_definitions(-DORTHANC_PUGIXML_ENABLED=1)
+if (STATIC_BUILD OR NOT USE_SYSTEM_PUGIXML)
+  set(PUGIXML_SOURCES_DIR ${CMAKE_BINARY_DIR}/pugixml-1.4)
+  set(PUGIXML_MD5 "7c56c91cfe3ecdee248a8e4892ef5781")
+  set(PUGIXML_URL "http://www.orthanc-server.com/downloads/third-party/pugixml-1.4.tar.gz")
 
-  if (STATIC_BUILD OR NOT USE_SYSTEM_PUGIXML)
-    set(PUGIXML_SOURCES_DIR ${CMAKE_BINARY_DIR}/pugixml-1.4)
-    set(PUGIXML_MD5 "7c56c91cfe3ecdee248a8e4892ef5781")
-    set(PUGIXML_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/pugixml-1.4.tar.gz")
+  DownloadPackage(${PUGIXML_MD5} ${PUGIXML_URL} "${PUGIXML_SOURCES_DIR}")
 
-    DownloadPackage(${PUGIXML_MD5} ${PUGIXML_URL} "${PUGIXML_SOURCES_DIR}")
+  include_directories(
+    ${PUGIXML_SOURCES_DIR}/src
+    )
 
-    include_directories(
-      ${PUGIXML_SOURCES_DIR}/src
-      )
+  set(PUGIXML_SOURCES
+    #${PUGIXML_SOURCES_DIR}/src/vlog_is_on.cc
+    ${PUGIXML_SOURCES_DIR}/src/pugixml.cpp
+    )
 
-    set(PUGIXML_SOURCES
-      #${PUGIXML_SOURCES_DIR}/src/vlog_is_on.cc
-      ${PUGIXML_SOURCES_DIR}/src/pugixml.cpp
-      )
+  source_group(ThirdParty\\pugixml REGULAR_EXPRESSION ${PUGIXML_SOURCES_DIR}/.*)
 
-  else()
-    CHECK_INCLUDE_FILE_CXX(pugixml.hpp HAVE_PUGIXML_H)
-    if (NOT HAVE_PUGIXML_H)
-      message(FATAL_ERROR "Please install the libpugixml-dev package")
-    endif()
-
-    link_libraries(pugixml)
+else()
+  CHECK_INCLUDE_FILE_CXX(pugixml.hpp HAVE_PUGIXML_H)
+  if (NOT HAVE_PUGIXML_H)
+    message(FATAL_ERROR "Please install the libpugixml-dev package")
   endif()
 
-else()
-  add_definitions(-DORTHANC_PUGIXML_ENABLED=0)
+  link_libraries(pugixml)
 endif()
--- a/Resources/CMake/SQLiteConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/SQLiteConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -15,9 +15,11 @@
 
 
 if (SQLITE_STATIC)
-  SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3071300)
-  SET(SQLITE_MD5 "5fbeff9645ab035a1f580e90b279a16d")
-  SET(SQLITE_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/sqlite-amalgamation-3071300.zip")
+  SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3210000)
+  SET(SQLITE_MD5 "fe330e88d81e77e1e61554a370ae5001")
+  SET(SQLITE_URL "http://www.orthanc-server.com/downloads/third-party/sqlite-amalgamation-3210000.zip")
+
+  add_definitions(-DORTHANC_SQLITE_VERSION=3021000)
 
   DownloadPackage(${SQLITE_MD5} ${SQLITE_URL} "${SQLITE_SOURCES_DIR}")
 
@@ -52,7 +54,10 @@
 
   # Autodetection of the version of SQLite
   file(STRINGS "${SQLITE_INCLUDE_DIR}/sqlite3.h" SQLITE_VERSION_NUMBER1 REGEX "#define SQLITE_VERSION_NUMBER.*$")    
-  string(REGEX REPLACE "#define SQLITE_VERSION_NUMBER(.*)$" "\\1" SQLITE_VERSION_NUMBER ${SQLITE_VERSION_NUMBER1})
+  string(REGEX REPLACE "#define SQLITE_VERSION_NUMBER(.*)$" "\\1" SQLITE_VERSION_NUMBER2 ${SQLITE_VERSION_NUMBER1})
+
+  # Remove the trailing spaces to convert the string to a proper integer
+  string(STRIP ${SQLITE_VERSION_NUMBER2} SQLITE_VERSION_NUMBER)
 
   message("Detected version of SQLite: ${SQLITE_VERSION_NUMBER}")
 
@@ -61,5 +66,7 @@
     message(FATAL_ERROR "SQLite version must be above 3.7.0. Please set the CMake variable USE_SYSTEM_SQLITE to OFF.")
   ENDIF()
 
+  add_definitions(-DORTHANC_SQLITE_VERSION=${SQLITE_VERSION_NUMBER})
+
   link_libraries(sqlite3)
 endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/UuidConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,102 @@
+if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+
+  if (STATIC_BUILD OR NOT USE_SYSTEM_UUID)
+    SET(E2FSPROGS_SOURCES_DIR ${CMAKE_BINARY_DIR}/e2fsprogs-1.43.8)
+    SET(E2FSPROGS_URL "http://www.orthanc-server.com/downloads/third-party/e2fsprogs-1.43.8.tar.gz")
+    SET(E2FSPROGS_MD5 "670b7a74a8ead5333acf21b9afc92b3c")
+
+    DownloadPackage(${E2FSPROGS_MD5} ${E2FSPROGS_URL} "${E2FSPROGS_SOURCES_DIR}")
+
+    include_directories(
+      ${E2FSPROGS_SOURCES_DIR}/lib
+      )
+
+    set(UUID_SOURCES
+      #${E2FSPROGS_SOURCES_DIR}/lib/uuid/tst_uuid.c
+      #${E2FSPROGS_SOURCES_DIR}/lib/uuid/uuid_time.c
+      ${E2FSPROGS_SOURCES_DIR}/lib/uuid/clear.c
+      ${E2FSPROGS_SOURCES_DIR}/lib/uuid/compare.c
+      ${E2FSPROGS_SOURCES_DIR}/lib/uuid/copy.c
+      ${E2FSPROGS_SOURCES_DIR}/lib/uuid/gen_uuid.c
+      ${E2FSPROGS_SOURCES_DIR}/lib/uuid/isnull.c
+      ${E2FSPROGS_SOURCES_DIR}/lib/uuid/pack.c
+      ${E2FSPROGS_SOURCES_DIR}/lib/uuid/parse.c
+      ${E2FSPROGS_SOURCES_DIR}/lib/uuid/unpack.c
+      ${E2FSPROGS_SOURCES_DIR}/lib/uuid/unparse.c
+      )
+
+    check_include_file("net/if.h"       HAVE_NET_IF_H)
+    check_include_file("net/if_dl.h"    HAVE_NET_IF_DL_H)
+    check_include_file("netinet/in.h"   HAVE_NETINET_IN_H)
+    check_include_file("stdlib.h"       HAVE_STDLIB_H)
+    check_include_file("sys/file.h"     HAVE_SYS_FILE_H)
+    check_include_file("sys/ioctl.h"    HAVE_SYS_IOCTL_H)
+    check_include_file("sys/resource.h" HAVE_SYS_RESOURCE_H)
+    check_include_file("sys/socket.h"   HAVE_SYS_SOCKET_H)
+    check_include_file("sys/sockio.h"   HAVE_SYS_SOCKIO_H)
+    check_include_file("sys/syscall.h"  HAVE_SYS_SYSCALL_H)
+    check_include_file("sys/time.h"     HAVE_SYS_TIME_H)
+    check_include_file("sys/un.h"       HAVE_SYS_UN_H)
+    check_include_file("unistd.h"       HAVE_UNISTD_H)
+
+    if (NOT HAVE_NET_IF_H)  # This is the case of OpenBSD
+      unset(HAVE_NET_IF_H CACHE)
+      check_include_files("sys/socket.h;net/if.h" HAVE_NET_IF_H)
+    endif()
+
+    if (NOT HAVE_NETINET_TCP_H)  # This is the case of OpenBSD
+      unset(HAVE_NETINET_TCP_H CACHE)
+      check_include_files("sys/socket.h;netinet/tcp.h" HAVE_NETINET_TCP_H)
+    endif()
+
+    if (NOT EXISTS ${E2FSPROGS_SOURCES_DIR}/lib/uuid/config.h)
+      file(WRITE ${E2FSPROGS_SOURCES_DIR}/lib/uuid/config.h.cmake "
+#cmakedefine HAVE_NET_IF_H \@HAVE_NET_IF_H\@
+#cmakedefine HAVE_NET_IF_DL_H \@HAVE_NET_IF_DL_H\@
+#cmakedefine HAVE_NETINET_IN_H \@HAVE_NETINET_IN_H\@
+#cmakedefine HAVE_STDLIB_H \@HAVE_STDLIB_H \@
+#cmakedefine HAVE_SYS_FILE_H \@HAVE_SYS_FILE_H\@
+#cmakedefine HAVE_SYS_IOCTL_H \@HAVE_SYS_IOCTL_H\@
+#cmakedefine HAVE_SYS_RESOURCE_H \@HAVE_SYS_RESOURCE_H\@
+#cmakedefine HAVE_SYS_SOCKET_H \@HAVE_SYS_SOCKET_H\@
+#cmakedefine HAVE_SYS_SOCKIO_H \@HAVE_SYS_SOCKIO_H\@
+#cmakedefine HAVE_SYS_SYSCALL_H \@HAVE_SYS_SYSCALL_H\@
+#cmakedefine HAVE_SYS_TIME_H \@HAVE_SYS_TIME_H\@
+#cmakedefine HAVE_SYS_UN_H \@HAVE_SYS_UN_H\@
+#cmakedefine HAVE_UNISTD_H \@HAVE_UNISTD_H\@
+")
+    endif()
+      
+    configure_file(
+      ${E2FSPROGS_SOURCES_DIR}/lib/uuid/config.h.cmake
+      ${E2FSPROGS_SOURCES_DIR}/lib/uuid/config.h
+      )
+      
+    configure_file(
+      ${E2FSPROGS_SOURCES_DIR}/lib/uuid/uuid.h.in
+      ${E2FSPROGS_SOURCES_DIR}/lib/uuid/uuid.h
+      )
+
+    if (NOT EXISTS ${E2FSPROGS_SOURCES_DIR}/lib/uuid/uuid_types.h)
+      file(WRITE
+        ${E2FSPROGS_SOURCES_DIR}/lib/uuid/uuid_types.h
+        "#include <stdint.h>\n")
+    endif()
+    
+    source_group(ThirdParty\\uuid REGULAR_EXPRESSION ${E2FSPROGS_SOURCES_DIR}/.*)
+
+  else()
+    CHECK_INCLUDE_FILE(uuid/uuid.h HAVE_UUID_H)
+    if (NOT HAVE_UUID_H)
+      message(FATAL_ERROR "Please install uuid-dev, e2fsprogs (OpenBSD) or e2fsprogs-libuuid (FreeBSD)")
+    endif()
+
+    check_library_exists(uuid uuid_generate_random "" HAVE_UUID_LIB)
+    if (NOT HAVE_UUID_LIB)
+      message(FATAL_ERROR "Unable to find the uuid library")
+    endif()
+    
+    link_libraries(uuid)
+  endif()
+
+endif()
--- a/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,4 +1,4 @@
-macro(ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS PrecompiledHeaders PrecompiledSource Sources)
+macro(ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS PrecompiledHeaders PrecompiledSource Sources Target)
   get_filename_component(PrecompiledBasename ${PrecompiledHeaders} NAME_WE)
   set(PrecompiledBinary "${PrecompiledBasename}_$(ConfigurationName).pch")
 
@@ -10,5 +10,5 @@
     PROPERTIES COMPILE_FLAGS "/Yu\"${PrecompiledHeaders}\" /FI\"${PrecompiledHeaders}\" /Fp\"${PrecompiledBinary}\""
     OBJECT_DEPENDS "${PrecompiledBinary}")
 
-  list(APPEND ${Sources} ${PrecompiledSource})
+  set(${Target} ${PrecompiledSource})
 endmacro()
--- a/Resources/CMake/ZlibConfiguration.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/CMake/ZlibConfiguration.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,7 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_ZLIB)
-  SET(ZLIB_SOURCES_DIR ${CMAKE_BINARY_DIR}/zlib-1.2.7)
-  SET(ZLIB_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/zlib-1.2.7.tar.gz")
-  SET(ZLIB_MD5 "60df6a37c56e7c1366cca812414f7b85")
+  SET(ZLIB_SOURCES_DIR ${CMAKE_BINARY_DIR}/zlib-1.2.11)
+  SET(ZLIB_URL "http://www.orthanc-server.com/downloads/third-party/zlib-1.2.11.tar.gz")
+  SET(ZLIB_MD5 "1c9f62f0778697a09d36121ead88e08e")
 
   DownloadPackage(${ZLIB_MD5} ${ZLIB_URL} "${ZLIB_SOURCES_DIR}")
 
@@ -27,10 +27,19 @@
     ${ZLIB_SOURCES_DIR}/zutil.c
     )
 
+  source_group(ThirdParty\\zlib REGULAR_EXPRESSION ${ZLIB_SOURCES_DIR}/.*)
+
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+    # "ioapi.c" from zlib (minizip) expects the "IOAPI_NO_64" macro to be set to "true"
+    # https://ohse.de/uwe/articles/lfs.html
+    add_definitions(
+      -DIOAPI_NO_64=1
+      )
+  endif()
+
 else()
   include(FindZLIB)
   include_directories(${ZLIB_INCLUDE_DIRS})
   link_libraries(${ZLIB_LIBRARIES})
 endif()
-
-source_group(ThirdParty\\ZLib REGULAR_EXPRESSION ${ZLIB_SOURCES_DIR}/.*)
--- a/Resources/Configuration.json	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Configuration.json	Fri Oct 12 15:18:10 2018 +0200
@@ -7,13 +7,14 @@
   // displayed in Orthanc Explorer and at the URI "/system".
   "Name" : "MyOrthanc",
 
-  // Path to the directory that holds the heavyweight files
-  // (i.e. the raw DICOM instances)
+  // Path to the directory that holds the heavyweight files (i.e. the
+  // raw DICOM instances). Backslashes must be either escaped by
+  // doubling them, or replaced by forward slashes "/".
   "StorageDirectory" : "OrthancStorage",
 
-  // Path to the directory that holds the SQLite index (if unset,
-  // the value of StorageDirectory is used). This index could be
-  // stored on a RAM-drive or a SSD device for performance reasons.
+  // Path to the directory that holds the SQLite index (if unset, the
+  // value of StorageDirectory is used). This index could be stored on
+  // a RAM-drive or a SSD device for performance reasons.
   "IndexDirectory" : "OrthancStorage",
 
   // Enable the transparent compression of the DICOM instances
@@ -37,10 +38,16 @@
   // instance of Orthanc (e.g. "./libPluginTest.so" for Linux, or
   // "./PluginTest.dll" for Windows). These paths can refer to
   // folders, in which case they will be scanned non-recursively to
-  // find shared libraries.
+  // find shared libraries. Backslashes must be either escaped by
+  // doubling them, or replaced by forward slashes "/".
   "Plugins" : [
   ],
 
+  // Maximum number of processing jobs that are simultaneously running
+  // at any given time. A value of "0" indicates to use all the
+  // available CPU logical cores. To emulate Orthanc <= 1.3.2, set
+  // this value to "1".
+  "ConcurrentJobs" : 2,
 
 
   /**
@@ -80,17 +87,19 @@
   // The DICOM Application Entity Title
   "DicomAet" : "ORTHANC",
 
-  // Check whether the called AET corresponds during a DICOM request
+  // Check whether the called AET corresponds to the AET of Orthanc
+  // during an incoming DICOM SCU request
   "DicomCheckCalledAet" : false,
 
   // The DICOM port
   "DicomPort" : 4242,
 
   // The default encoding that is assumed for DICOM files without
-  // "SpecificCharacterSet" DICOM tag. The allowed values are "Ascii",
-  // "Utf8", "Latin1", "Latin2", "Latin3", "Latin4", "Latin5",
-  // "Cyrillic", "Windows1251", "Arabic", "Greek", "Hebrew", "Thai",
-  // "Japanese", and "Chinese".
+  // "SpecificCharacterSet" DICOM tag, and that is used when answering
+  // C-Find requests (including worklists). The allowed values are
+  // "Ascii", "Utf8", "Latin1", "Latin2", "Latin3", "Latin4",
+  // "Latin5", "Cyrillic", "Windows1251", "Arabic", "Greek", "Hebrew",
+  // "Thai", "Japanese", and "Chinese".
   "DefaultEncoding" : "Latin1",
 
   // The transfer syntaxes that are accepted by Orthanc C-Store SCP
@@ -102,6 +111,15 @@
   "Mpeg2TransferSyntaxAccepted"        : true,
   "RleTransferSyntaxAccepted"          : true,
 
+  // Whether Orthanc accepts to act as C-Store SCP for unknown storage
+  // SOP classes (aka. "promiscuous mode")
+  "UnknownSopClassAccepted"            : false,
+
+  // Set the timeout (in seconds) after which the DICOM associations
+  // are closed by the Orthanc SCP (server) if no further DIMSE
+  // command is received from the SCU (client).
+  "DicomScpTimeout" : 30,
+
 
 
   /**
@@ -114,7 +132,8 @@
   // Whether or not SSL is enabled
   "SslEnabled" : false,
 
-  // Path to the SSL certificate (meaningful only if SSL is enabled)
+  // Path to the SSL certificate in the PEM format (meaningful only if
+  // SSL is enabled)
   "SslCertificate" : "certificate.pem",
 
   // Whether or not the password protection is enabled
@@ -140,18 +159,62 @@
      * store (shipped in the DCMTK distribution) started by the
      * command line "storescp 2000".
      **/
-    // "sample" : [ "STORESCP", "localhost", 2000 ]
+    // "sample" : [ "STORESCP", "127.0.0.1", 2000 ]
+
+    /**
+     * A fourth parameter is available to enable patches for
+     * specific PACS manufacturers. The allowed values are currently:
+     * - "Generic" (default value),
+     * - "GenericNoWildcardInDates" (to replace "*" by "" in date fields 
+     *   in outgoing C-Find requests originating from Orthanc)
+     * - "GenericNoUniversalWildcard" (to replace "*" by "" in all fields
+     *   in outgoing C-Find SCU requests originating from Orthanc)
+     * - "StoreScp" (storescp tool from DCMTK),
+     * - "ClearCanvas", "Dcm4Chee" and "Vitrea".
+     * This parameter is case-sensitive.
+     **/
+    // "clearcanvas" : [ "CLEARCANVAS", "192.168.1.1", 104, "ClearCanvas" ]
 
     /**
-     * A fourth parameter is available to enable patches for a
-     * specific PACS manufacturer. The allowed values are currently
-     * "Generic" (default value), "StoreScp" (storescp tool from
-     * DCMTK), "ClearCanvas", "MedInria", "Dcm4Chee" and
-     * "SyngoVia". This parameter is case-sensitive.
+     * By default, the Orthanc SCP accepts all DICOM commands (C-GET,
+     * C-STORE, C-FIND, C-MOVE) issued by the remote SCU
+     * modalities. Starting with Orthanc 1.4.3, it is possible to
+     * specify which DICOM commands are allowed, separately for each
+     * remote modality, using the syntax below.
      **/
-    // "clearcanvas" : [ "CLEARCANVAS", "192.168.1.1", 104, "ClearCanvas" ]
+    //"untrusted" : {
+    //  "AET" : "ORTHANC",
+    //  "Port" : 104,
+    //  "Host" : "127.0.0.1",
+    //  "AllowEcho" : false,
+    //  "AllowFind" : false,
+    //  "AllowMove" : false,
+    //  "AllowStore" : true
+    //}
   },
 
+  // Whether the Orthanc SCP allows incoming C-Echo requests, even
+  // from SCU modalities it does not know about (i.e. that are not
+  // listed in the "DicomModalities" option above). Orthanc 1.3.0
+  // is the only version to behave as if this argument was set to "false".
+  "DicomAlwaysAllowEcho" : true,
+
+  // Whether the Orthanc SCP allows incoming C-Store requests, even
+  // from SCU modalities it does not know about (i.e. that are not
+  // listed in the "DicomModalities" option above)
+  "DicomAlwaysAllowStore" : true,
+
+  // Whether Orthanc checks the IP/hostname address of the remote
+  // modality initiating a DICOM connection (as listed in the
+  // "DicomModalities" option above). If this option is set to
+  // "false", Orthanc only checks the AET of the remote modality.
+  "DicomCheckModalityHost" : false,
+
+  // The timeout (in seconds) after which the DICOM associations are
+  // considered as closed by the Orthanc SCU (client) if the remote
+  // DICOM SCP (server) does not answer.
+  "DicomScuTimeout" : 10,
+
   // The list of the known Orthanc peers
   "OrthancPeers" : {
     /**
@@ -159,8 +222,25 @@
      * followed by the username/password pair (if the password
      * protection is enabled on the peer).
      **/
-    // "peer"  : [ "http://localhost:8043/", "alice", "alicePassword" ]
-    // "peer2" : [ "http://localhost:8044/" ]
+    // "peer"  : [ "http://127.0.0.1:8043/", "alice", "alicePassword" ]
+    // "peer2" : [ "http://127.0.0.1:8044/" ]
+
+    /**
+     * This is another, more advanced format to define Orthanc
+     * peers. It notably allows to specify HTTP headers, a HTTPS
+     * client certificate in the PEM format (as in the "--cert" option
+     * of curl), or to enable PKCS#11 authentication for smart cards.
+     **/
+    // "peer" : {
+    //   "Url" : "http://127.0.0.1:8043/",
+    //   "Username" : "alice",
+    //   "Password" : "alicePassword",
+    //   "HttpHeaders" : { "Token" : "Hello world" },
+    //   "CertificateFile" : "client.crt",
+    //   "CertificateKeyFile" : "client.key",
+    //   "CertificateKeyPassword" : "certpass",
+    //   "Pkcs11" : false
+    // }
   },
 
   // Parameters of the HTTP proxy to be used by Orthanc. If set to the
@@ -169,10 +249,18 @@
   //   "HttpProxy" : "proxyUser:proxyPassword@192.168.0.1:3128"
   "HttpProxy" : "",
 
+  // If set to "true", debug messages from libcurl will be issued
+  // whenever Orthanc makes an outgoing HTTP request. This is notably
+  // useful to debug HTTPS-related problems.
+  "HttpVerbose" : false,
+
   // Set the timeout for HTTP requests issued by Orthanc (in seconds).
   "HttpTimeout" : 10,
 
-  // Enable the verification of the peers during HTTPS requests.
+  // Enable the verification of the peers during HTTPS requests. This
+  // option must be set to "false" if using self-signed certificates.
+  // Pay attention that setting this option to "false" results in
+  // security risks!
   // Reference: http://curl.haxx.se/docs/sslcerts.html
   "HttpsVerifyPeers" : true,
 
@@ -190,17 +278,20 @@
    **/
 
   // Dictionary of symbolic names for the user-defined metadata. Each
-  // entry must map a number between 1024 and 65535 to an unique
-  // string.
+  // entry must map an unique string to an unique number between 1024
+  // and 65535. Reserved values:
+  //  - The Orthanc whole-slide imaging plugin uses metadata 4200
   "UserMetadata" : {
     // "Sample" : 1024
   },
 
   // Dictionary of symbolic names for the user-defined types of
-  // attached files. Each entry must map a number between 1024 and
-  // 65535 to an unique string.
+  // attached files. Each entry must map an unique string to an unique
+  // number between 1024 and 65535. Optionally, a second argument can
+  // provided to specify a MIME content type for the attachment.
   "UserContentType" : {
     // "sample" : 1024
+    // "sample2" : [ 1025, "application/pdf" ]
   },
 
   // Number of seconds without receiving any instance before a
@@ -232,11 +323,13 @@
   // some job finishes.
   "LimitJobs" : 10,
 
-  // If this option is set to "false", Orthanc will not log the
-  // resources that are exported to other DICOM modalities of Orthanc
-  // peers in the URI "/exports". This is useful to prevent the index
-  // to grow indefinitely in auto-routing tasks.
-  "LogExportedResources" : true,
+  // If this option is set to "true" (default behavior until Orthanc
+  // 1.3.2), Orthanc will log the resources that are exported to other
+  // DICOM modalities or Orthanc peers, inside the URI
+  // "/exports". Setting this option to "false" is useful to prevent
+  // the index to grow indefinitely in auto-routing tasks (this is the
+  // default behavior since Orthanc 1.4.0).
+  "LogExportedResources" : false,
 
   // Enable or disable HTTP Keep-Alive (deprecated). Set this option
   // to "true" only in the case of high HTTP loads.
@@ -247,10 +340,11 @@
   // this option might prevent the upgrade to newer versions of Orthanc.
   "StoreDicom" : true,
 
-  // DICOM associations are kept open as long as new DICOM commands
-  // are issued. This option sets the number of seconds of inactivity
-  // to wait before automatically closing a DICOM association. If set
-  // to 0, the connection is closed immediately.
+  // DICOM associations initiated by Lua scripts are kept open as long
+  // as new DICOM commands are issued. This option sets the number of
+  // seconds of inactivity to wait before automatically closing a
+  // DICOM association used by Lua. If set to 0, the connection is
+  // closed immediately.
   "DicomAssociationCloseDelay" : 5,
 
   // Maximum number of query/retrieve DICOM requests that are
@@ -262,5 +356,69 @@
   // will enable case-sensitive match for PN value representation
   // (such as PatientName). By default, the search is
   // case-insensitive, which does not follow the DICOM standard.
-  "CaseSensitivePN" : false
+  "CaseSensitivePN" : false,
+
+  // Configure PKCS#11 to use hardware security modules (HSM) and
+  // smart cards when carrying on HTTPS client authentication.
+  /**
+     "Pkcs11" : {
+       "Module" : "/usr/local/lib/libbeidpkcs11.so",
+       "Module" : "C:/Windows/System32/beidpkcs11.dll",
+       "Pin" : "1234",
+       "Verbose" : true
+     }
+   **/
+  
+  // If set to "true", Orthanc will still handle "SOP Classes in
+  // Study" (0008,0062) in C-FIND requests, even if the "SOP Class
+  // UID" metadata is not available in the database (which is the case
+  // if the DB was previously used by Orthanc <= 1.1.0). This option
+  // is turned off by default, as it requires intensive accesses to
+  // the hard drive.
+  "AllowFindSopClassesInStudy" : false,
+
+  // If set to "false", Orthanc will not load its default dictionary
+  // of private tags. This might be necessary if you cannot import a
+  // DICOM file encoded using the Implicit VR Endian transfer syntax,
+  // and containing private tags: Such an import error might stem from
+  // a bad dictionary. You can still list your private tags of
+  // interest in the "Dictionary" configuration option below.
+  "LoadPrivateDictionary" : true,
+
+  // Locale to be used by Orthanc. Currently, only used if comparing
+  // strings in a case-insensitive way. It should be safe to keep this
+  // value undefined, which lets Orthanc autodetect the suitable locale.
+  // "Locale" : "en_US.UTF-8",
+
+  // Register a new tag in the dictionary of DICOM tags that are known
+  // to Orthanc. Each line must contain the tag (formatted as 2
+  // hexadecimal numbers), the value representation (2 upcase
+  // characters), a nickname for the tag, possibly the minimum
+  // multiplicity (> 0 with defaults to 1), possibly the maximum
+  // multiplicity (0 means arbitrary multiplicity, defaults to 1), and
+  // possibly the Private Creator (for private tags).
+  "Dictionary" : {
+    // "0014,1020" : [ "DA", "ValidationExpiryDate", 1, 1 ]
+    // "00e1,10c2" : [ "UI", "PET-CT Multi Modality Name", 1, 1, "ELSCINT1" ]
+    // "7053,1003" : [ "ST", "Original Image Filename", 1, 1, "Philips PET Private Group" ]
+    // "2001,5f" : [ "SQ", "StackSequence", 1, 1, "Philips Imaging DD 001" ]
+  },
+
+  // Whether to run DICOM C-Move operations synchronously. If set to
+  // "false" (the default), each incoming C-Move request results in
+  // creating a new background job. Up to Orthanc 1.3.2, the implicit
+  // behavior was to use synchronous C-Move.
+  "SynchronousCMove" : false,
+
+  // Maximum number of completed jobs that are kept in memory. A
+  // processing job is considered as complete once it is tagged as
+  // "Success" or "Failure".
+  "JobsHistorySize" : 10,
+
+  // Specifies how Orthanc reacts when it receives a DICOM instance
+  // whose SOPInstanceUID is already stored. If set to "true", the new
+  // instance replaces the old one. If set to "false", the new
+  // instance is discarded and the old one is kept. Up to Orthanc
+  // 1.4.1, the implicit behavior corresponded to "false".
+  "OverwriteInstances" : false
 }
--- a/Resources/DicomConformanceStatement.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/DicomConformanceStatement.py	Fri Oct 12 15:18:10 2018 +0200
@@ -1,8 +1,9 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Resources/DicomConformanceStatement.txt	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/DicomConformanceStatement.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -149,6 +149,7 @@
 
   FINDPatientRootQueryRetrieveInformationModel   | 1.2.840.10008.5.1.4.1.2.1.1
   FINDStudyRootQueryRetrieveInformationModel     | 1.2.840.10008.5.1.4.1.2.2.1
+  FINDModalityWorklistInformationModel           | 1.2.840.10008.5.1.4.31
 
 
 --------------------
@@ -202,7 +203,7 @@
 Transfer Syntaxes
 -----------------
 
-Orthanc will accept and negociate presentation contexts for all of the
+Orthanc will accept and negotiate presentation contexts for all of the
 abovementioned supported SOP Classes using any of the following
 transfer syntaxes:
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/DownloadOrthancFramework.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,327 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# In addition, as a special exception, the copyright holders of this
+# program give permission to link the code of its release with the
+# OpenSSL project's "OpenSSL" library (or with modified versions of it
+# that use the same license as the "OpenSSL" library), and distribute
+# the linked executables. You must obey the GNU General Public License
+# in all respects for all of the code used other than "OpenSSL". If you
+# modify file(s) with this exception, you may extend this exception to
+# your version of the file(s), but you are not obligated to do so. If
+# you do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source files
+# in the program, then also delete it here.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+
+##
+## Check whether the parent script sets the mandatory variables
+##
+
+if (NOT DEFINED ORTHANC_FRAMEWORK_SOURCE OR
+    (NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" AND
+     NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "web" AND
+     NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" AND
+     NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "path"))
+  message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_SOURCE must be set to \"hg\", \"web\", \"archive\" or \"path\"")
+endif()
+
+
+##
+## Detection of the requested version
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" OR
+    ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
+    ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
+  if (NOT DEFINED ORTHANC_FRAMEWORK_VERSION)
+    message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_VERSION must be set")
+  endif()
+
+  if (DEFINED ORTHANC_FRAMEWORK_MAJOR OR
+      DEFINED ORTHANC_FRAMEWORK_MINOR OR
+      DEFINED ORTHANC_FRAMEWORK_REVISION OR
+      DEFINED ORTHANC_FRAMEWORK_MD5)
+    message(FATAL_ERROR "Some internal variable has been set")
+  endif()
+
+  set(ORTHANC_FRAMEWORK_MD5 "")
+
+  if (NOT DEFINED ORTHANC_FRAMEWORK_BRANCH)
+    if (ORTHANC_FRAMEWORK_VERSION STREQUAL "mainline")
+      set(ORTHANC_FRAMEWORK_BRANCH "default")
+
+    else()
+      set(ORTHANC_FRAMEWORK_BRANCH "Orthanc-${ORTHANC_FRAMEWORK_VERSION}")
+
+      set(RE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$")
+      string(REGEX REPLACE ${RE} "\\1" ORTHANC_FRAMEWORK_MAJOR ${ORTHANC_FRAMEWORK_VERSION})
+      string(REGEX REPLACE ${RE} "\\2" ORTHANC_FRAMEWORK_MINOR ${ORTHANC_FRAMEWORK_VERSION})
+      string(REGEX REPLACE ${RE} "\\3" ORTHANC_FRAMEWORK_REVISION ${ORTHANC_FRAMEWORK_VERSION})
+
+      if (NOT ORTHANC_FRAMEWORK_MAJOR MATCHES "^[0-9]+$" OR
+          NOT ORTHANC_FRAMEWORK_MINOR MATCHES "^[0-9]+$" OR
+          NOT ORTHANC_FRAMEWORK_REVISION MATCHES "^[0-9]+$")
+        message("Bad version of the Orthanc framework: ${ORTHANC_FRAMEWORK_VERSION}")
+      endif()
+
+      if (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.3.1")
+        set(ORTHANC_FRAMEWORK_MD5 "dac95bd6cf86fb19deaf4e612961f378")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.3.2")
+        set(ORTHANC_FRAMEWORK_MD5 "d0ccdf68e855d8224331f13774992750")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.0")
+        set(ORTHANC_FRAMEWORK_MD5 "81e15f34d97ac32bbd7d26e85698835a")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.2")
+        set(ORTHANC_FRAMEWORK_MD5 "d1ee84927dcf668e60eb5868d24b9394")
+      endif()
+    endif()
+  endif()
+endif()
+
+
+
+##
+## Detection of the third-party software
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg")
+  find_program(ORTHANC_FRAMEWORK_HG hg)
+  
+  if (${ORTHANC_FRAMEWORK_HG} MATCHES "ORTHANC_FRAMEWORK_HG-NOTFOUND")
+    message(FATAL_ERROR "Please install Mercurial")
+  endif()
+endif()
+
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
+    ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
+  if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
+    find_program(ORTHANC_FRAMEWORK_7ZIP 7z 
+      PATHS 
+      "$ENV{ProgramFiles}/7-Zip"
+      "$ENV{ProgramW6432}/7-Zip"
+      )
+
+    if (${ORTHANC_FRAMEWORK_7ZIP} MATCHES "ORTHANC_FRAMEWORK_7ZIP-NOTFOUND")
+      message(FATAL_ERROR "Please install the '7-zip' software (http://www.7-zip.org/)")
+    endif()
+
+  else()
+    find_program(ORTHANC_FRAMEWORK_TAR tar)
+    if (${ORTHANC_FRAMEWORK_TAR} MATCHES "ORTHANC_FRAMEWORK_TAR-NOTFOUND")
+      message(FATAL_ERROR "Please install the 'tar' package")
+    endif()
+  endif()
+endif()
+
+
+
+##
+## Case of the Orthanc framework specified as a path on the filesystem
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "path")
+  if (NOT DEFINED ORTHANC_FRAMEWORK_ROOT)
+    message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_ROOT must provide the path to the sources of Orthanc")
+  endif()
+  
+  if (NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT})
+    message(FATAL_ERROR "Non-existing directory: ${ORTHANC_FRAMEWORK_ROOT}")
+  endif()
+  
+  if (NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT}/Resources/CMake/OrthancFrameworkParameters.cmake)
+    message(FATAL_ERROR "Directory not containing the source code of Orthanc: ${ORTHANC_FRAMEWORK_ROOT}")
+  endif()
+  
+  set(ORTHANC_ROOT ${ORTHANC_FRAMEWORK_ROOT})
+endif()
+
+
+
+##
+## Case of the Orthanc framework cloned using Mercurial
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg")
+  if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS)
+    message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON")
+  endif()
+
+  set(ORTHANC_ROOT ${CMAKE_BINARY_DIR}/orthanc)
+
+  if (EXISTS ${ORTHANC_ROOT})
+    message("Updating the Orthanc source repository using Mercurial")
+    execute_process(
+      COMMAND ${ORTHANC_FRAMEWORK_HG} pull
+      WORKING_DIRECTORY ${ORTHANC_ROOT}
+      RESULT_VARIABLE Failure
+      )    
+  else()
+    message("Forking the Orthanc source repository using Mercurial")
+    execute_process(
+      COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://bitbucket.org/sjodogne/orthanc"
+      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+      RESULT_VARIABLE Failure
+      )    
+  endif()
+
+  if (Failure OR NOT EXISTS ${ORTHANC_ROOT})
+    message(FATAL_ERROR "Cannot fork the Orthanc repository")
+  endif()
+
+  message("Setting branch of the Orthanc repository to: ${ORTHANC_FRAMEWORK_BRANCH}")
+
+  execute_process(
+    COMMAND ${ORTHANC_FRAMEWORK_HG} update -c ${ORTHANC_FRAMEWORK_BRANCH}
+    WORKING_DIRECTORY ${ORTHANC_ROOT}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while running Mercurial")
+  endif()
+endif()
+
+
+
+##
+## Case of the Orthanc framework provided as a source archive on the
+## filesystem
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive")
+  if (NOT DEFINED ORTHANC_FRAMEWORK_ARCHIVE)
+    message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_ARCHIVE must provide the path to the sources of Orthanc")
+  endif()
+endif()
+
+
+
+##
+## Case of the Orthanc framework downloaded from the Web
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
+  if (DEFINED ORTHANC_FRAMEWORK_URL)
+    string(REGEX REPLACE "^.*/" "" ORTHANC_FRAMEMORK_FILENAME "${ORTHANC_FRAMEWORK_URL}")
+  else()
+    # Default case: Download from the official Web site
+    set(ORTHANC_FRAMEMORK_FILENAME Orthanc-${ORTHANC_FRAMEWORK_VERSION}.tar.gz)
+    #set(ORTHANC_FRAMEWORK_URL "http://www.orthanc-server.com/downloads/get.php?path=/orthanc/${ORTHANC_FRAMEMORK_FILENAME}")
+    set(ORTHANC_FRAMEWORK_URL "http://www.orthanc-server.com/downloads/third-party/orthanc-framework/${ORTHANC_FRAMEMORK_FILENAME}")
+  endif()
+
+  set(ORTHANC_FRAMEWORK_ARCHIVE "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${ORTHANC_FRAMEMORK_FILENAME}")
+
+  if (NOT EXISTS "${ORTHANC_FRAMEWORK_ARCHIVE}")
+    if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS)
+      message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON")
+    endif()
+
+    message("Downloading: ${ORTHANC_FRAMEWORK_URL}")
+
+    file(DOWNLOAD
+      "${ORTHANC_FRAMEWORK_URL}" "${ORTHANC_FRAMEWORK_ARCHIVE}" 
+      SHOW_PROGRESS EXPECTED_MD5 "${ORTHANC_FRAMEWORK_MD5}"
+      TIMEOUT 60
+      INACTIVITY_TIMEOUT 60
+      )
+  else()
+    message("Using local copy of: ${ORTHANC_FRAMEWORK_URL}")
+  endif()  
+endif()
+
+
+
+
+##
+## Uncompressing the Orthanc framework, if it was retrieved from a
+## source archive on the filesystem, or from the official Web site
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
+    ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
+
+  if (NOT DEFINED ORTHANC_FRAMEWORK_ARCHIVE OR
+      NOT DEFINED ORTHANC_FRAMEWORK_VERSION OR
+      NOT DEFINED ORTHANC_FRAMEWORK_MD5)
+    message(FATAL_ERROR "Internal error")
+  endif()
+
+  if (ORTHANC_FRAMEWORK_MD5 STREQUAL "")
+    message(FATAL_ERROR "Unknown release of Orthanc: ${ORTHANC_FRAMEWORK_VERSION}")
+  endif()
+
+  file(MD5 ${ORTHANC_FRAMEWORK_ARCHIVE} ActualMD5)
+
+  if (NOT "${ActualMD5}" STREQUAL "${ORTHANC_FRAMEWORK_MD5}")
+    message(FATAL_ERROR "The MD5 hash of the Orthanc archive is invalid: ${ORTHANC_FRAMEWORK_ARCHIVE}")
+  endif()
+
+  set(ORTHANC_ROOT "${CMAKE_BINARY_DIR}/Orthanc-${ORTHANC_FRAMEWORK_VERSION}")
+
+  if (NOT IS_DIRECTORY "${ORTHANC_ROOT}")
+    if (NOT ORTHANC_FRAMEWORK_ARCHIVE MATCHES ".tar.gz$")
+      message(FATAL_ERROR "Archive should have the \".tar.gz\" extension: ${ORTHANC_FRAMEWORK_ARCHIVE}")
+    endif()
+    
+    message("Uncompressing: ${ORTHANC_FRAMEWORK_ARCHIVE}")
+
+    if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
+      # How to silently extract files using 7-zip
+      # http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly
+
+      execute_process(
+        COMMAND ${ORTHANC_FRAMEWORK_7ZIP} e -y ${ORTHANC_FRAMEWORK_ARCHIVE}
+        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+        RESULT_VARIABLE Failure
+        OUTPUT_QUIET
+        )
+      
+      if (Failure)
+        message(FATAL_ERROR "Error while running the uncompression tool")
+      endif()
+
+      get_filename_component(TMP_FILENAME "${ORTHANC_FRAMEWORK_ARCHIVE}" NAME)
+      string(REGEX REPLACE ".gz$" "" TMP_FILENAME2 "${TMP_FILENAME}")
+
+      execute_process(
+        COMMAND ${ORTHANC_FRAMEWORK_7ZIP} x -y ${TMP_FILENAME2}
+        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+        RESULT_VARIABLE Failure
+        OUTPUT_QUIET
+        )
+
+    else()
+      execute_process(
+        COMMAND sh -c "${ORTHANC_FRAMEWORK_TAR} xfz ${ORTHANC_FRAMEWORK_ARCHIVE}"
+        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+        RESULT_VARIABLE Failure
+        )
+    endif()
+   
+    if (Failure)
+      message(FATAL_ERROR "Error while running the uncompression tool")
+    endif()
+
+    if (NOT IS_DIRECTORY "${ORTHANC_ROOT}")
+      message(FATAL_ERROR "The Orthanc framework was not uncompressed at the proper location. Check the CMake instructions.")
+    endif()
+  endif()
+endif()
--- a/Resources/EmbedResources.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/EmbedResources.py	Fri Oct 12 15:18:10 2018 +0200
@@ -1,8 +1,9 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -39,8 +40,8 @@
 UPCASE_CHECK = True
 USE_SYSTEM_EXCEPTION = False
 EXCEPTION_CLASS = 'OrthancException'
-OUT_OF_RANGE_EXCEPTION = 'OrthancException(ErrorCode_ParameterOutOfRange)'
-INEXISTENT_PATH_EXCEPTION = 'OrthancException(ErrorCode_InexistentItem)'
+OUT_OF_RANGE_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_ParameterOutOfRange)'
+INEXISTENT_PATH_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_InexistentItem)'
 NAMESPACE = 'Orthanc'
 
 ARGS = []
@@ -159,6 +160,10 @@
 #include <string>
 #include <list>
 
+#if defined(_MSC_VER)
+#  pragma warning(disable: 4065)  // "Switch statement contains 'default' but no 'case' labels"
+#endif
+
 namespace %s
 {
   namespace EmbeddedResources
--- a/Resources/EncodingTests.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/EncodingTests.h	Fri Oct 12 15:18:10 2018 +0200
@@ -59,3 +59,5 @@
   "\xd0\xa2\xd0\xb0\xd0\xb7",
   "\xd0\x9f\xd1\x80\xd1\x8f\xd0\xbc\xd0\xb0\xd1\x8f"
 };
+static const char *toUpperSource = "\x67\x72\xc3\xbc\xc3\x9f\x45\x4e\x20\x53\xc3\xa9\x62\x61\x73\x54\x49\x65\x6e\x20\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xd0\x94\xce\x98\xc4\x9d\xd7\x93\xd8\xb5\xc4\xb7\xd1\x9b\xe0\xb9\x9b\xef\xbe\x88\xc4\xb0";
+static const char *toUpperResult = "\x47\x52\xc3\x9c\xc3\x9f\x45\x4e\x20\x53\xc3\x89\x42\x41\x53\x54\x49\x45\x4e\x20\x54\x45\x53\x54\xc3\x89\xc3\x84\xc3\x96\xc3\x92\xd0\x94\xce\x98\xc4\x9c\xd7\x93\xd8\xb5\xc4\xb6\xd0\x8b\xe0\xb9\x9b\xef\xbe\x88\xc4\xb0";
--- a/Resources/EncodingTests.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/EncodingTests.py	Fri Oct 12 15:18:10 2018 +0200
@@ -72,3 +72,9 @@
     for i in range(len(expected)):
         print expected[i]
         #print '%s: %s' % (expected[i], l[i])
+
+
+
+u = (u'grüßEN SébasTIen %s' % source)
+print 'static const char *toUpperSource = %s;' % ToArray(u.encode('utf-8'))
+print 'static const char *toUpperResult = %s;' % ToArray(u.upper().encode('utf-8'))
--- a/Resources/ErrorCodes.json	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/ErrorCodes.json	Fri Oct 12 15:18:10 2018 +0200
@@ -32,7 +32,7 @@
   {
     "Code": 4, 
     "Name": "NotEnoughMemory", 
-    "Description": "Not enough memory"
+    "Description": "The server hosting Orthanc is running out of memory"
   }, 
   {
     "Code": 5, 
@@ -189,7 +189,29 @@
     "Code": 33,
     "Name": "EmptyRequest",
     "Description": "The request is empty"
+  }, 
+  {
+    "Code": 34, 
+    "HttpStatus": 406, 
+    "Name": "NotAcceptable", 
+    "Description": "Cannot send a response which is acceptable according to the Accept HTTP header"
+  }, 
+  {
+    "Code": 35, 
+    "Name": "NullPointer", 
+    "Description": "Cannot handle a NULL pointer"
   },
+  {
+    "Code": 36, 
+    "HttpStatus": 503, 
+    "Name": "DatabaseUnavailable", 
+    "Description": "The database is currently not available (probably a transient situation)"
+  }, 
+  {
+    "Code": 37, 
+    "Name": "CanceledJob", 
+    "Description": "This job was canceled"
+  }, 
 
 
 
@@ -321,12 +343,12 @@
   {
     "Code": 2003, 
     "Name": "HttpPortInUse", 
-    "Description": "The TCP port of the HTTP server is already in use"
+    "Description": "The TCP port of the HTTP server is privileged or already in use"
   },
   {
     "Code": 2004, 
     "Name": "DicomPortInUse", 
-    "Description": "The TCP port of the DICOM server is already in use"
+    "Description": "The TCP port of the DICOM server is privileged or already in use"
   },
   {
     "Code": 2005, 
@@ -507,5 +529,15 @@
     "Code": 2040,
     "Name": "CannotOrderSlices",
     "Description": "Unable to order the slices of the series"
+  },
+  {
+    "Code": 2041, 
+    "Name": "NoWorklistHandler", 
+    "Description": "No request handler factory for DICOM C-Find Modality SCP"
+  },
+  {
+    "Code": 2042,
+    "Name": "AlreadyExistingTag",
+    "Description": "Cannot override the value of a tag that already exists"
   }
 ]
--- a/Resources/Fonts/GenerateFont.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Fonts/GenerateFont.py	Fri Oct 12 15:18:10 2018 +0200
@@ -1,8 +1,9 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/GenerateAnonymizationProfile.py	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import re
+import sys
+import xml.etree.ElementTree as ET
+
+# Usage:
+# ./GenerateAnonymizationProfile.py ~/Subversion/dicom-specification/2017c/part15.xml 
+
+if len(sys.argv) != 2:
+    raise Exception('Please provide the path to the part15.xml file from the DICOM standard')
+
+with open(sys.argv[1], 'r') as f:
+    root = ET.fromstring(f.read())
+
+br = '{http://docbook.org/ns/docbook}' # Shorthand variable
+
+
+LINES = []
+
+def FormatLine(command, name):
+    indentation = 65
+    
+    if len(command) > indentation:
+        raise Exception('Too long command')
+        
+    line = '    ' + command + (' ' * (indentation - len(command))) + '// ' + name
+    LINES.append(line)
+
+def FormatUnknown(rawTag, name, profile):
+    FormatLine('// TODO: %s with rule %s' % (rawTag, profile), name)
+
+    
+RAW_TAG_RE = re.compile(r'^\(\s*([0-9A-F]{4})\s*,\s*([0-9A-F]{4})\s*\)$')
+
+
+for table in root.iter('%stable' % br):
+    if table.attrib['label'] == 'E.1-1':
+        for row in table.find('%stbody' % br).iter('%str' % br):
+            rawTag = row.find('%std[2]/%spara' % (br, br)).text
+            name = row.find('%std[1]/%spara' % (br, br)).text
+            profile = row.find('%std[5]/%spara' % (br, br)).text
+
+            if len(name.strip()) == 0:
+                continue
+
+            match = RAW_TAG_RE.match(rawTag)
+            if match == None:
+                FormatUnknown(rawTag, name, profile)
+            else:
+                tag = '0x%s, 0x%s' % (match.group(1).lower(), match.group(2).lower())
+
+                if name in [
+                        'SOP Instance UID',
+                        'Series Instance UID',
+                        'Study Instance UID',
+                ]:
+                    FormatLine('// Tag (%s) is set in Apply()         /* %s */' % (tag, profile), name)
+                elif name in [
+                        'Referenced Image Sequence',
+                        'Source Image Sequence',
+                        'Referenced SOP Instance UID',
+                        'Frame of Reference UID',
+                        'Referenced Frame of Reference UID',
+                        'Related Frame of Reference UID',
+                ]:
+                    FormatLine('// Tag (%s) => RelationshipsVisitor   /* %s */' % (tag, profile), name)
+                elif name in [
+                        'Patient\'s Name',
+                        'Patient ID',
+                ]:
+                    FormatLine('// Tag (%s) is set below (*)          /* %s */' % (tag, profile), name)
+                elif profile == 'X':
+                    FormatLine('removals_.insert(DicomTag(%s));' % tag, name)
+                elif profile.startswith('X/'):
+                    FormatLine('removals_.insert(DicomTag(%s));   /* %s */' % (tag, profile), name)
+                elif profile == 'Z':
+                    FormatLine('clearings_.insert(DicomTag(%s));' % tag, name)
+                elif profile == 'D' or profile.startswith('Z/'):
+                    FormatLine('clearings_.insert(DicomTag(%s));  /* %s */' % (tag, profile), name)
+                elif profile == 'U':
+                    FormatLine('removals_.insert(DicomTag(%s));   /* TODO UID */' % (tag), name)
+                else:
+                    FormatUnknown(rawTag, name, profile)
+
+for line in sorted(LINES):
+    print line
+    
+
+# D - replace with a non-zero length value that may be a dummy value and consistent with the VR
+# Z - replace with a zero length value, or a non-zero length value that may be a dummy value and consistent with the VR
+# X - remove
+# K - keep (unchanged for non-sequence attributes, cleaned for sequences)
+# C - clean, that is replace with values of similar meaning known not to contain identifying information and consistent with the VR
+# U - replace with a non-zero length UID that is internally consistent within a set of Instances
+# Z/D - Z unless D is required to maintain IOD conformance (Type 2 versus Type 1)
+# X/Z - X unless Z is required to maintain IOD conformance (Type 3 versus Type 2)
+# X/D - X unless D is required to maintain IOD conformance (Type 3 versus Type 1)
+# X/Z/D - X unless Z or D is required to maintain IOD conformance (Type 3 versus Type 2 versus Type 1)
+# X/Z/U* - X unless Z or replacement of contained instance UIDs (U) is required to maintain IOD conformance (Type 3 versus Type 2 versus Type 1 sequences containing UID references)
--- a/Resources/GenerateErrorCodes.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/GenerateErrorCodes.py	Fri Oct 12 15:18:10 2018 +0200
@@ -1,8 +1,9 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/DatabasePluginSample/CMakeLists.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,74 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(SampleDatabasePlugin)
+
+# Parameters of the build
+SET(SAMPLE_DATABASE_VERSION "0.0" CACHE STRING "Version of the plugin")
+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")
+SET(STANDALONE_BUILD ON)
+
+# 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_SQLITE ON CACHE BOOL "Use the system version of SQLite")
+
+set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
+include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake)
+
+include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/SQLiteConfiguration.cmake)
+
+EmbedResources(
+  --system-exception  # Use "std::runtime_error" instead of "OrthancException" for embedded resources
+  PREPARE_DATABASE  ${ORTHANC_ROOT}/OrthancServer/PrepareDatabase.sql
+  )
+
+message("Setting the version of the plugin to ${SAMPLE_DATABASE_VERSION}")
+
+add_definitions(
+  -DORTHANC_SQLITE_STANDALONE=1
+  -DORTHANC_ENABLE_BASE64=0
+  -DORTHANC_ENABLE_LOGGING=0
+  -DORTHANC_ENABLE_MD5=0
+  -DORTHANC_ENABLE_PLUGINS=1
+  -DORTHANC_ENABLE_PUGIXML=0
+  -DORTHANC_SANDBOXED=0
+  -DSAMPLE_DATABASE_VERSION="${SAMPLE_DATABASE_VERSION}"
+  )
+
+add_library(SampleDatabase SHARED 
+  ${BOOST_SOURCES}
+  ${JSONCPP_SOURCES}
+  ${SQLITE_SOURCES}
+  ${AUTOGENERATED_SOURCES}
+
+  ${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/SQLite/Connection.cpp
+  ${ORTHANC_ROOT}/Core/SQLite/FunctionContext.cpp
+  ${ORTHANC_ROOT}/Core/SQLite/Statement.cpp
+  ${ORTHANC_ROOT}/Core/SQLite/StatementId.cpp
+  ${ORTHANC_ROOT}/Core/SQLite/StatementReference.cpp
+  ${ORTHANC_ROOT}/Core/SQLite/Transaction.cpp
+  ${ORTHANC_ROOT}/Core/Toolbox.cpp
+  ${ORTHANC_ROOT}/OrthancServer/DatabaseWrapperBase.cpp
+  ${ORTHANC_ROOT}/Plugins/Engine/PluginsEnumerations.cpp
+
+  Database.cpp
+  Plugin.cpp
+  )
+
+set_target_properties(SampleDatabase PROPERTIES 
+  VERSION ${SAMPLE_DATABASE_VERSION} 
+  SOVERSION ${SAMPLE_DATABASE_VERSION})
+
+install(
+  TARGETS SampleDatabase
+  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/Resources/Graveyard/DatabasePluginSample/Database.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,565 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "Database.h"
+
+#include "../../../Core/DicomFormat/DicomArray.h"
+
+#include <EmbeddedResources.h>
+#include <boost/lexical_cast.hpp>
+
+
+namespace Internals
+{
+  class SignalFileDeleted : public Orthanc::SQLite::IScalarFunction
+  {
+  private:
+    OrthancPlugins::DatabaseBackendOutput&  output_;
+
+  public:
+    SignalFileDeleted(OrthancPlugins::DatabaseBackendOutput&  output) : output_(output)
+    {
+    }
+
+    virtual const char* GetName() const
+    {
+      return "SignalFileDeleted";
+    }
+
+    virtual unsigned int GetCardinality() const
+    {
+      return 7;
+    }
+
+    virtual void Compute(Orthanc::SQLite::FunctionContext& context)
+    {
+      std::string uncompressedMD5, compressedMD5;
+
+      if (!context.IsNullValue(5))
+      {
+        uncompressedMD5 = context.GetStringValue(5);
+      }
+
+      if (!context.IsNullValue(6))
+      {
+        compressedMD5 = context.GetStringValue(6);
+      }
+      
+      output_.SignalDeletedAttachment(context.GetStringValue(0),
+                                      context.GetIntValue(1),
+                                      context.GetInt64Value(2),
+                                      uncompressedMD5,
+                                      context.GetIntValue(3),
+                                      context.GetInt64Value(4),
+                                      compressedMD5);
+    }
+  };
+
+
+  class SignalResourceDeleted : public Orthanc::SQLite::IScalarFunction
+  {
+  private:
+    OrthancPlugins::DatabaseBackendOutput&  output_;
+
+  public:
+    SignalResourceDeleted(OrthancPlugins::DatabaseBackendOutput&  output) : output_(output)
+    {
+    }
+
+    virtual const char* GetName() const
+    {
+      return "SignalResourceDeleted";
+    }
+
+    virtual unsigned int GetCardinality() const
+    {
+      return 2;
+    }
+
+    virtual void Compute(Orthanc::SQLite::FunctionContext& context)
+    {
+      output_.SignalDeletedResource(context.GetStringValue(0),
+                                    Orthanc::Plugins::Convert(static_cast<Orthanc::ResourceType>(context.GetIntValue(1))));
+    }
+  };
+}
+
+
+class Database::SignalRemainingAncestor : public Orthanc::SQLite::IScalarFunction
+{
+private:
+  bool hasRemainingAncestor_;
+  std::string remainingPublicId_;
+  OrthancPluginResourceType remainingType_;
+
+public:
+  SignalRemainingAncestor() : 
+    hasRemainingAncestor_(false),
+    remainingType_(OrthancPluginResourceType_Instance)  // Some dummy value
+  {
+  }
+
+  void Reset()
+  {
+    hasRemainingAncestor_ = false;
+  }
+
+  virtual const char* GetName() const
+  {
+    return "SignalRemainingAncestor";
+  }
+
+  virtual unsigned int GetCardinality() const
+  {
+    return 2;
+  }
+
+  virtual void Compute(Orthanc::SQLite::FunctionContext& context)
+  {
+    if (!hasRemainingAncestor_ ||
+        remainingType_ >= context.GetIntValue(1))
+    {
+      hasRemainingAncestor_ = true;
+      remainingPublicId_ = context.GetStringValue(0);
+      remainingType_ = Orthanc::Plugins::Convert(static_cast<Orthanc::ResourceType>(context.GetIntValue(1)));
+    }
+  }
+
+  bool HasRemainingAncestor() const
+  {
+    return hasRemainingAncestor_;
+  }
+
+  const std::string& GetRemainingAncestorId() const
+  {
+    assert(hasRemainingAncestor_);
+    return remainingPublicId_;
+  }
+
+  OrthancPluginResourceType GetRemainingAncestorType() const
+  {
+    assert(hasRemainingAncestor_);
+    return remainingType_;
+  }
+};
+
+
+
+Database::Database(const std::string& path) : 
+  path_(path),
+  base_(db_),
+  signalRemainingAncestor_(NULL)
+{
+}
+
+
+void Database::Open()
+{
+  db_.Open(path_);
+
+  db_.Execute("PRAGMA ENCODING=\"UTF-8\";");
+
+  // http://www.sqlite.org/pragma.html
+  db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;");
+  db_.Execute("PRAGMA JOURNAL_MODE=WAL;");
+  db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;");
+  db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;");
+  //db_.Execute("PRAGMA TEMP_STORE=memory");
+
+  if (!db_.DoesTableExist("GlobalProperties"))
+  {
+    std::string query;
+    Orthanc::EmbeddedResources::GetFileResource(query, Orthanc::EmbeddedResources::PREPARE_DATABASE);
+    db_.Execute(query);
+  }
+
+  signalRemainingAncestor_ = new SignalRemainingAncestor;
+  db_.Register(signalRemainingAncestor_);
+  db_.Register(new Internals::SignalFileDeleted(GetOutput()));
+  db_.Register(new Internals::SignalResourceDeleted(GetOutput()));
+}
+
+
+void Database::Close()
+{
+  db_.Close();
+}
+
+
+void Database::AddAttachment(int64_t id,
+                             const OrthancPluginAttachment& attachment)
+{
+  Orthanc::FileInfo info(attachment.uuid,
+                         static_cast<Orthanc::FileContentType>(attachment.contentType),
+                         attachment.uncompressedSize,
+                         attachment.uncompressedHash,
+                         static_cast<Orthanc::CompressionType>(attachment.compressionType),
+                         attachment.compressedSize,
+                         attachment.compressedHash);
+  base_.AddAttachment(id, info);
+}
+
+
+void Database::DeleteResource(int64_t id)
+{
+  signalRemainingAncestor_->Reset();
+
+  Orthanc::SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?");
+  s.BindInt64(0, id);
+  s.Run();
+
+  if (signalRemainingAncestor_->HasRemainingAncestor())
+  {
+    GetOutput().SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorId(),
+                                        signalRemainingAncestor_->GetRemainingAncestorType());
+  }
+}
+
+
+static void Answer(OrthancPlugins::DatabaseBackendOutput& output,
+                   const Orthanc::ServerIndexChange& change)
+{
+  output.AnswerChange(change.GetSeq(), 
+                      change.GetChangeType(),
+                      Orthanc::Plugins::Convert(change.GetResourceType()),
+                      change.GetPublicId(),
+                      change.GetDate());
+}
+
+
+static void Answer(OrthancPlugins::DatabaseBackendOutput& output,
+                   const Orthanc::ExportedResource& resource)
+{
+  output.AnswerExportedResource(resource.GetSeq(),
+                                Orthanc::Plugins::Convert(resource.GetResourceType()),
+                                resource.GetPublicId(),
+                                resource.GetModality(),
+                                resource.GetDate(),
+                                resource.GetPatientId(),
+                                resource.GetStudyInstanceUid(),
+                                resource.GetSeriesInstanceUid(),
+                                resource.GetSopInstanceUid());
+}
+
+
+void Database::GetChanges(bool& done /*out*/,
+                          int64_t since,
+                          uint32_t maxResults)
+{
+  typedef std::list<Orthanc::ServerIndexChange> Changes;
+
+  Changes changes;
+  base_.GetChanges(changes, done, since, maxResults);
+
+  for (Changes::const_iterator it = changes.begin(); it != changes.end(); ++it)
+  {
+    Answer(GetOutput(), *it);
+  }
+}
+
+
+void Database::GetExportedResources(bool& done /*out*/,
+                                    int64_t since,
+                                    uint32_t maxResults)
+{
+  typedef std::list<Orthanc::ExportedResource> Resources;
+
+  Resources resources;
+  base_.GetExportedResources(resources, done, since, maxResults);
+
+  for (Resources::const_iterator it = resources.begin(); it != resources.end(); ++it)
+  {
+    Answer(GetOutput(), *it);
+  }
+}
+
+
+void Database::GetLastChange()
+{
+  std::list<Orthanc::ServerIndexChange> change;
+  Orthanc::ErrorCode code = base_.GetLastChange(change);
+  
+  if (code != Orthanc::ErrorCode_Success)
+  {
+    throw OrthancPlugins::DatabaseException(static_cast<OrthancPluginErrorCode>(code));
+  }
+
+  if (!change.empty())
+  {
+    Answer(GetOutput(), change.front());
+  }
+}
+
+
+void Database::GetLastExportedResource()
+{
+  std::list<Orthanc::ExportedResource> resource;
+  base_.GetLastExportedResource(resource);
+  
+  if (!resource.empty())
+  {
+    Answer(GetOutput(), resource.front());
+  }
+}
+
+
+void Database::GetMainDicomTags(int64_t id)
+{
+  Orthanc::DicomMap tags;
+  base_.GetMainDicomTags(tags, id);
+
+  Orthanc::DicomArray arr(tags);
+  for (size_t i = 0; i < arr.GetSize(); i++)
+  {
+    GetOutput().AnswerDicomTag(arr.GetElement(i).GetTag().GetGroup(),
+                               arr.GetElement(i).GetTag().GetElement(),
+                               arr.GetElement(i).GetValue().GetContent());
+  }
+}
+
+
+std::string Database::GetPublicId(int64_t resourceId)
+{
+  std::string id;
+  if (base_.GetPublicId(id, resourceId))
+  {
+    return id;
+  }
+  else
+  {
+    throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_UnknownResource);
+  }
+}
+
+
+OrthancPluginResourceType Database::GetResourceType(int64_t resourceId)
+{
+  Orthanc::ResourceType  result;
+  Orthanc::ErrorCode  code = base_.GetResourceType(result, resourceId);
+
+  if (code == Orthanc::ErrorCode_Success)
+  {
+    return Orthanc::Plugins::Convert(result);
+  }
+  else
+  {
+    throw OrthancPlugins::DatabaseException(static_cast<OrthancPluginErrorCode>(code));
+  }
+}
+
+
+
+template <typename I>
+static void ConvertList(std::list<int32_t>& target,
+                        const std::list<I>& source)
+{
+  for (typename std::list<I>::const_iterator 
+         it = source.begin(); it != source.end(); ++it)
+  {
+    target.push_back(*it);
+  }
+}
+
+
+void Database::ListAvailableMetadata(std::list<int32_t>& target /*out*/,
+                                     int64_t id)
+{
+  std::list<Orthanc::MetadataType> tmp;
+  base_.ListAvailableMetadata(tmp, id);
+  ConvertList(target, tmp);
+}
+
+
+void Database::ListAvailableAttachments(std::list<int32_t>& target /*out*/,
+                                        int64_t id)
+{
+  std::list<Orthanc::FileContentType> tmp;
+  base_.ListAvailableAttachments(tmp, id);
+  ConvertList(target, tmp);
+}
+
+
+void Database::LogChange(const OrthancPluginChange& change)
+{
+  int64_t id;
+  OrthancPluginResourceType type;
+  if (!LookupResource(id, type, change.publicId) ||
+      type != change.resourceType)
+  {
+    throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_DatabasePlugin);
+  }
+
+  Orthanc::ServerIndexChange tmp(change.seq,
+                                 static_cast<Orthanc::ChangeType>(change.changeType),
+                                 Orthanc::Plugins::Convert(change.resourceType),
+                                 change.publicId,
+                                 change.date);
+
+  base_.LogChange(id, tmp);
+}
+
+
+void Database::LogExportedResource(const OrthancPluginExportedResource& resource) 
+{
+  Orthanc::ExportedResource tmp(resource.seq,
+                                Orthanc::Plugins::Convert(resource.resourceType),
+                                resource.publicId,
+                                resource.modality,
+                                resource.date,
+                                resource.patientId,
+                                resource.studyInstanceUid,
+                                resource.seriesInstanceUid,
+                                resource.sopInstanceUid);
+
+  base_.LogExportedResource(tmp);
+}
+
+    
+bool Database::LookupAttachment(int64_t id,
+                                int32_t contentType)
+{
+  Orthanc::FileInfo attachment;
+  if (base_.LookupAttachment(attachment, id, static_cast<Orthanc::FileContentType>(contentType)))
+  {
+    GetOutput().AnswerAttachment(attachment.GetUuid(),
+                                 attachment.GetContentType(),
+                                 attachment.GetUncompressedSize(),
+                                 attachment.GetUncompressedMD5(),
+                                 attachment.GetCompressionType(),
+                                 attachment.GetCompressedSize(),
+                                 attachment.GetCompressedMD5());
+    return true;
+  }
+  else
+  {
+    return false;
+  }
+}
+
+
+bool Database::LookupParent(int64_t& parentId /*out*/,
+                            int64_t resourceId)
+{
+  bool found;
+  Orthanc::ErrorCode code = base_.LookupParent(found, parentId, resourceId);
+
+  if (code == Orthanc::ErrorCode_Success)
+  {
+    return found;
+  }
+  else
+  {
+    throw OrthancPlugins::DatabaseException(static_cast<OrthancPluginErrorCode>(code));
+  }
+}
+
+
+bool Database::LookupResource(int64_t& id /*out*/,
+                              OrthancPluginResourceType& type /*out*/,
+                              const char* publicId)
+{
+  Orthanc::ResourceType tmp;
+  if (base_.LookupResource(id, tmp, publicId))
+  {
+    type = Orthanc::Plugins::Convert(tmp);
+    return true;
+  }
+  else
+  {
+    return false;
+  }
+}
+
+
+void Database::StartTransaction()
+{
+  transaction_.reset(new Orthanc::SQLite::Transaction(db_));
+  transaction_->Begin();
+}
+
+
+void Database::RollbackTransaction()
+{
+  transaction_->Rollback();
+  transaction_.reset(NULL);
+}
+
+
+void Database::CommitTransaction()
+{
+  transaction_->Commit();
+  transaction_.reset(NULL);
+}
+
+
+uint32_t Database::GetDatabaseVersion()
+{
+  std::string version;
+
+  if (!LookupGlobalProperty(version, Orthanc::GlobalProperty_DatabaseSchemaVersion))
+  {
+    throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_InternalError);
+  }
+
+  try
+  {
+    return boost::lexical_cast<uint32_t>(version);
+  }
+  catch (boost::bad_lexical_cast&)
+  {
+    throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_InternalError);
+  }
+}
+
+
+void Database::UpgradeDatabase(uint32_t  targetVersion,
+                               OrthancPluginStorageArea* storageArea)
+{
+  if (targetVersion == 6)
+  {
+    OrthancPluginErrorCode code = OrthancPluginReconstructMainDicomTags(GetOutput().GetContext(), storageArea, 
+                                                                        OrthancPluginResourceType_Study);
+    if (code == OrthancPluginErrorCode_Success)
+    {
+      code = OrthancPluginReconstructMainDicomTags(GetOutput().GetContext(), storageArea, 
+                                                   OrthancPluginResourceType_Series);
+    }
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      throw OrthancPlugins::DatabaseException(code);
+    }
+
+    base_.SetGlobalProperty(Orthanc::GlobalProperty_DatabaseSchemaVersion, "6");
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/DatabasePluginSample/Database.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,285 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <orthanc/OrthancCppDatabasePlugin.h>
+
+#include "../../../Core/SQLite/Connection.h"
+#include "../../../Core/SQLite/Transaction.h"
+#include "../../../OrthancServer/DatabaseWrapperBase.h"
+#include "../../Engine/PluginsEnumerations.h"
+
+#include <memory>
+
+class Database : public OrthancPlugins::IDatabaseBackend
+{
+private:
+  class SignalRemainingAncestor;
+
+  std::string                   path_;
+  Orthanc::SQLite::Connection   db_;
+  Orthanc::DatabaseWrapperBase  base_;
+  SignalRemainingAncestor*      signalRemainingAncestor_;
+
+  std::auto_ptr<Orthanc::SQLite::Transaction>  transaction_;
+
+public:
+  Database(const std::string& path);
+
+  virtual void Open();
+
+  virtual void Close();
+
+  virtual void AddAttachment(int64_t id,
+                             const OrthancPluginAttachment& attachment);
+
+  virtual void AttachChild(int64_t parent,
+                           int64_t child)
+  {
+    base_.AttachChild(parent, child);
+  }
+
+  virtual void ClearChanges()
+  {
+    db_.Execute("DELETE FROM Changes");    
+  }
+
+  virtual void ClearExportedResources()
+  {
+    db_.Execute("DELETE FROM ExportedResources");    
+  }
+
+  virtual int64_t CreateResource(const char* publicId,
+                                 OrthancPluginResourceType type)
+  {
+    return base_.CreateResource(publicId, Orthanc::Plugins::Convert(type));
+  }
+
+  virtual void DeleteAttachment(int64_t id,
+                                int32_t attachment)
+  {
+    base_.DeleteAttachment(id, static_cast<Orthanc::FileContentType>(attachment));
+  }
+
+  virtual void DeleteMetadata(int64_t id,
+                              int32_t metadataType)
+  {
+    base_.DeleteMetadata(id, static_cast<Orthanc::MetadataType>(metadataType));
+  }
+
+  virtual void DeleteResource(int64_t id);
+
+  virtual void GetAllInternalIds(std::list<int64_t>& target,
+                                 OrthancPluginResourceType resourceType)
+  {
+    base_.GetAllInternalIds(target, Orthanc::Plugins::Convert(resourceType));
+  }
+
+  virtual void GetAllPublicIds(std::list<std::string>& target,
+                               OrthancPluginResourceType resourceType)
+  {
+    base_.GetAllPublicIds(target, Orthanc::Plugins::Convert(resourceType));
+  }
+
+  virtual void GetAllPublicIds(std::list<std::string>& target,
+                               OrthancPluginResourceType resourceType,
+                               uint64_t since,
+                               uint64_t limit)
+  {
+    base_.GetAllPublicIds(target, Orthanc::Plugins::Convert(resourceType), since, limit);
+  }
+
+  virtual void GetChanges(bool& done /*out*/,
+                          int64_t since,
+                          uint32_t maxResults);
+
+  virtual void GetChildrenInternalId(std::list<int64_t>& target /*out*/,
+                                     int64_t id)
+  {
+    base_.GetChildrenInternalId(target, id);
+  }
+
+  virtual void GetChildrenPublicId(std::list<std::string>& target /*out*/,
+                                   int64_t id)
+  {
+    base_.GetChildrenPublicId(target, id);
+  }
+
+  virtual void GetExportedResources(bool& done /*out*/,
+                                    int64_t since,
+                                    uint32_t maxResults);
+
+  virtual void GetLastChange();
+
+  virtual void GetLastExportedResource();
+
+  virtual void GetMainDicomTags(int64_t id);
+
+  virtual std::string GetPublicId(int64_t resourceId);
+
+  virtual uint64_t GetResourceCount(OrthancPluginResourceType resourceType)
+  {
+    return base_.GetResourceCount(Orthanc::Plugins::Convert(resourceType));
+  }
+
+  virtual OrthancPluginResourceType GetResourceType(int64_t resourceId);
+
+  virtual uint64_t GetTotalCompressedSize()
+  {
+    return base_.GetTotalCompressedSize();
+  }
+    
+  virtual uint64_t GetTotalUncompressedSize()
+  {
+    return base_.GetTotalUncompressedSize();
+  }
+
+  virtual bool IsExistingResource(int64_t internalId)
+  {
+    return base_.IsExistingResource(internalId);
+  }
+
+  virtual bool IsProtectedPatient(int64_t internalId)
+  {
+    return base_.IsProtectedPatient(internalId);
+  }
+
+  virtual void ListAvailableMetadata(std::list<int32_t>& target /*out*/,
+                                     int64_t id);
+
+  virtual void ListAvailableAttachments(std::list<int32_t>& target /*out*/,
+                                        int64_t id);
+
+  virtual void LogChange(const OrthancPluginChange& change);
+
+  virtual void LogExportedResource(const OrthancPluginExportedResource& resource);
+    
+  virtual bool LookupAttachment(int64_t id,
+                                int32_t contentType);
+
+  virtual bool LookupGlobalProperty(std::string& target /*out*/,
+                                    int32_t property)
+  {
+    return base_.LookupGlobalProperty(target, static_cast<Orthanc::GlobalProperty>(property));
+  }
+
+  virtual void LookupIdentifier(std::list<int64_t>& target /*out*/,
+                                OrthancPluginResourceType level,
+                                uint16_t group,
+                                uint16_t element,
+                                OrthancPluginIdentifierConstraint constraint,
+                                const char* value)
+  {
+    base_.LookupIdentifier(target, Orthanc::Plugins::Convert(level),
+                           Orthanc::DicomTag(group, element), 
+                           Orthanc::Plugins::Convert(constraint), value);
+  }
+
+  virtual bool LookupMetadata(std::string& target /*out*/,
+                              int64_t id,
+                              int32_t metadataType)
+  {
+    return base_.LookupMetadata(target, id, static_cast<Orthanc::MetadataType>(metadataType));
+  }
+
+  virtual bool LookupParent(int64_t& parentId /*out*/,
+                            int64_t resourceId);
+
+  virtual bool LookupResource(int64_t& id /*out*/,
+                              OrthancPluginResourceType& type /*out*/,
+                              const char* publicId);
+
+  virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/)
+  {
+    return base_.SelectPatientToRecycle(internalId);
+  }
+
+  virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/,
+                                      int64_t patientIdToAvoid)
+  {
+    return base_.SelectPatientToRecycle(internalId, patientIdToAvoid);
+  }
+
+
+  virtual void SetGlobalProperty(int32_t property,
+                                 const char* value)
+  {
+    base_.SetGlobalProperty(static_cast<Orthanc::GlobalProperty>(property), value);
+  }
+
+  virtual void SetMainDicomTag(int64_t id,
+                               uint16_t group,
+                               uint16_t element,
+                               const char* value)
+  {
+    base_.SetMainDicomTag(id, Orthanc::DicomTag(group, element), value);
+  }
+
+  virtual void SetIdentifierTag(int64_t id,
+                                uint16_t group,
+                                uint16_t element,
+                                const char* value)
+  {
+    base_.SetIdentifierTag(id, Orthanc::DicomTag(group, element), value);
+  }
+
+  virtual void SetMetadata(int64_t id,
+                           int32_t metadataType,
+                           const char* value)
+  {
+    base_.SetMetadata(id, static_cast<Orthanc::MetadataType>(metadataType), value);
+  }
+
+  virtual void SetProtectedPatient(int64_t internalId, 
+                                   bool isProtected)
+  {
+    base_.SetProtectedPatient(internalId, isProtected);
+  }
+
+  virtual void StartTransaction();
+
+  virtual void RollbackTransaction();
+
+  virtual void CommitTransaction();
+
+  virtual uint32_t GetDatabaseVersion();
+
+  virtual void UpgradeDatabase(uint32_t  targetVersion,
+                               OrthancPluginStorageArea* storageArea);
+
+  virtual void ClearMainDicomTags(int64_t internalId)
+  {
+    base_.ClearMainDicomTags(internalId);
+  }
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,757 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeadersServer.h"
+#include "DatabaseWrapperBase.h"
+
+#include <stdio.h>
+#include <memory>
+
+namespace Orthanc
+{
+  void DatabaseWrapperBase::SetGlobalProperty(GlobalProperty property,
+                                              const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)");
+    s.BindInt(0, property);
+    s.BindString(1, value);
+    s.Run();
+  }
+
+  bool DatabaseWrapperBase::LookupGlobalProperty(std::string& target,
+                                                 GlobalProperty property)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT value FROM GlobalProperties WHERE property=?");
+    s.BindInt(0, property);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      target = s.ColumnString(0);
+      return true;
+    }
+  }
+
+  int64_t DatabaseWrapperBase::CreateResource(const std::string& publicId,
+                                              ResourceType type)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)");
+    s.BindInt(0, type);
+    s.BindString(1, publicId);
+    s.Run();
+    return db_.GetLastInsertRowId();
+  }
+
+  bool DatabaseWrapperBase::LookupResource(int64_t& id,
+                                           ResourceType& type,
+                                           const std::string& publicId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT internalId, resourceType FROM Resources WHERE publicId=?");
+    s.BindString(0, publicId);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      id = s.ColumnInt(0);
+      type = static_cast<ResourceType>(s.ColumnInt(1));
+
+      // Check whether there is a single resource with this public id
+      assert(!s.Step());
+
+      return true;
+    }
+  }
+
+  ErrorCode DatabaseWrapperBase::LookupParent(bool& found,
+                                              int64_t& parentId,
+                                              int64_t resourceId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT parentId FROM Resources WHERE internalId=?");
+    s.BindInt64(0, resourceId);
+
+    if (!s.Step())
+    {
+      return ErrorCode_UnknownResource;
+    }
+
+    if (s.ColumnIsNull(0))
+    {
+      found = false;
+    }
+    else
+    {
+      found = true;
+      parentId = s.ColumnInt(0);
+    }
+
+    return ErrorCode_Success;
+  }
+
+  bool DatabaseWrapperBase::GetPublicId(std::string& result,
+                                        int64_t resourceId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT publicId FROM Resources WHERE internalId=?");
+    s.BindInt64(0, resourceId);
+    
+    if (!s.Step())
+    { 
+      return false;
+    }
+    else
+    {
+      result = s.ColumnString(0);
+      return true;
+    }
+  }
+
+
+  ErrorCode DatabaseWrapperBase::GetResourceType(ResourceType& result,
+                                                 int64_t resourceId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT resourceType FROM Resources WHERE internalId=?");
+    s.BindInt64(0, resourceId);
+    
+    if (s.Step())
+    {
+      result = static_cast<ResourceType>(s.ColumnInt(0));
+      return ErrorCode_Success;
+    }
+    else
+    { 
+      return ErrorCode_UnknownResource;
+    }
+  }
+
+
+  void DatabaseWrapperBase::AttachChild(int64_t parent,
+                                        int64_t child)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?");
+    s.BindInt64(0, parent);
+    s.BindInt64(1, child);
+    s.Run();
+  }
+
+
+  void DatabaseWrapperBase::SetMetadata(int64_t id,
+                                        MetadataType type,
+                                        const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)");
+    s.BindInt64(0, id);
+    s.BindInt(1, type);
+    s.BindString(2, value);
+    s.Run();
+  }
+
+  void DatabaseWrapperBase::DeleteMetadata(int64_t id,
+                                           MetadataType type)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?");
+    s.BindInt64(0, id);
+    s.BindInt(1, type);
+    s.Run();
+  }
+
+  bool DatabaseWrapperBase::LookupMetadata(std::string& target,
+                                           int64_t id,
+                                           MetadataType type)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT value FROM Metadata WHERE id=? AND type=?");
+    s.BindInt64(0, id);
+    s.BindInt(1, type);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      target = s.ColumnString(0);
+      return true;
+    }
+  }
+
+  void DatabaseWrapperBase::ListAvailableMetadata(std::list<MetadataType>& target,
+                                                  int64_t id)
+  {
+    target.clear();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type FROM Metadata WHERE id=?");
+    s.BindInt64(0, id);
+
+    while (s.Step())
+    {
+      target.push_back(static_cast<MetadataType>(s.ColumnInt(0)));
+    }
+  }
+
+
+  void DatabaseWrapperBase::AddAttachment(int64_t id,
+                                          const FileInfo& attachment)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
+    s.BindInt64(0, id);
+    s.BindInt(1, attachment.GetContentType());
+    s.BindString(2, attachment.GetUuid());
+    s.BindInt64(3, attachment.GetCompressedSize());
+    s.BindInt64(4, attachment.GetUncompressedSize());
+    s.BindInt(5, attachment.GetCompressionType());
+    s.BindString(6, attachment.GetUncompressedMD5());
+    s.BindString(7, attachment.GetCompressedMD5());
+    s.Run();
+  }
+
+
+  void DatabaseWrapperBase::DeleteAttachment(int64_t id,
+                                             FileContentType attachment)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?");
+    s.BindInt64(0, id);
+    s.BindInt(1, attachment);
+    s.Run();
+  }
+
+
+
+  void DatabaseWrapperBase::ListAvailableAttachments(std::list<FileContentType>& target,
+                                                     int64_t id)
+  {
+    target.clear();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT fileType FROM AttachedFiles WHERE id=?");
+    s.BindInt64(0, id);
+
+    while (s.Step())
+    {
+      target.push_back(static_cast<FileContentType>(s.ColumnInt(0)));
+    }
+  }
+
+  bool DatabaseWrapperBase::LookupAttachment(FileInfo& attachment,
+                                             int64_t id,
+                                             FileContentType contentType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT uuid, uncompressedSize, compressionType, compressedSize, uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?");
+    s.BindInt64(0, id);
+    s.BindInt(1, contentType);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      attachment = FileInfo(s.ColumnString(0),
+                            contentType,
+                            s.ColumnInt64(1),
+                            s.ColumnString(4),
+                            static_cast<CompressionType>(s.ColumnInt(2)),
+                            s.ColumnInt64(3),
+                            s.ColumnString(5));
+      return true;
+    }
+  }
+
+
+  void DatabaseWrapperBase::ClearMainDicomTags(int64_t id)
+  {
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?");
+      s.BindInt64(0, id);
+      s.Run();
+    }
+
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?");
+      s.BindInt64(0, id);
+      s.Run();
+    }
+  }
+
+
+  void DatabaseWrapperBase::SetMainDicomTag(int64_t id,
+                                            const DicomTag& tag,
+                                            const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
+    s.BindInt64(0, id);
+    s.BindInt(1, tag.GetGroup());
+    s.BindInt(2, tag.GetElement());
+    s.BindString(3, value);
+    s.Run();
+  }
+
+
+  void DatabaseWrapperBase::SetIdentifierTag(int64_t id,
+                                             const DicomTag& tag,
+                                             const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)");
+    s.BindInt64(0, id);
+    s.BindInt(1, tag.GetGroup());
+    s.BindInt(2, tag.GetElement());
+    s.BindString(3, value);
+    s.Run();
+  }
+
+
+  void DatabaseWrapperBase::GetMainDicomTags(DicomMap& map,
+                                             int64_t id)
+  {
+    map.Clear();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?");
+    s.BindInt64(0, id);
+    while (s.Step())
+    {
+      map.SetValue(s.ColumnInt(1),
+                   s.ColumnInt(2),
+                   s.ColumnString(3), false);
+    }
+  }
+
+
+
+  void DatabaseWrapperBase::GetChildrenPublicId(std::list<std::string>& target,
+                                                int64_t id)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b  "
+                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
+    s.BindInt64(0, id);
+
+    target.clear();
+
+    while (s.Step())
+    {
+      target.push_back(s.ColumnString(0));
+    }
+  }
+
+
+  void DatabaseWrapperBase::GetChildrenInternalId(std::list<int64_t>& target,
+                                                  int64_t id)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b  "
+                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
+    s.BindInt64(0, id);
+
+    target.clear();
+
+    while (s.Step())
+    {
+      target.push_back(s.ColumnInt64(0));
+    }
+  }
+
+
+  void DatabaseWrapperBase::LogChange(int64_t internalId,
+                                      const ServerIndexChange& change)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)");
+    s.BindInt(0, change.GetChangeType());
+    s.BindInt64(1, internalId);
+    s.BindInt(2, change.GetResourceType());
+    s.BindString(3, change.GetDate());
+    s.Run();
+  }
+
+
+  ErrorCode DatabaseWrapperBase::GetChangesInternal(std::list<ServerIndexChange>& target,
+                                                    bool& done,
+                                                    SQLite::Statement& s,
+                                                    uint32_t maxResults)
+  {
+    target.clear();
+
+    while (target.size() < maxResults && s.Step())
+    {
+      int64_t seq = s.ColumnInt64(0);
+      ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
+      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3));
+      const std::string& date = s.ColumnString(4);
+
+      int64_t internalId = s.ColumnInt64(2);
+      std::string publicId;
+      if (!GetPublicId(publicId, internalId))
+      {
+        return ErrorCode_UnknownResource;
+      }
+
+      target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
+    }
+
+    done = !(target.size() == maxResults && s.Step());
+    return ErrorCode_Success;
+  }
+
+
+  ErrorCode DatabaseWrapperBase::GetChanges(std::list<ServerIndexChange>& target,
+                                            bool& done,
+                                            int64_t since,
+                                            uint32_t maxResults)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
+    s.BindInt64(0, since);
+    s.BindInt(1, maxResults + 1);
+    return GetChangesInternal(target, done, s, maxResults);
+  }
+
+  ErrorCode DatabaseWrapperBase::GetLastChange(std::list<ServerIndexChange>& target)
+  {
+    bool done;  // Ignored
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
+    return GetChangesInternal(target, done, s, 1);
+  }
+
+
+  void DatabaseWrapperBase::LogExportedResource(const ExportedResource& resource)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)");
+
+    s.BindInt(0, resource.GetResourceType());
+    s.BindString(1, resource.GetPublicId());
+    s.BindString(2, resource.GetModality());
+    s.BindString(3, resource.GetPatientId());
+    s.BindString(4, resource.GetStudyInstanceUid());
+    s.BindString(5, resource.GetSeriesInstanceUid());
+    s.BindString(6, resource.GetSopInstanceUid());
+    s.BindString(7, resource.GetDate());
+    s.Run();      
+  }
+
+
+  void DatabaseWrapperBase::GetExportedResourcesInternal(std::list<ExportedResource>& target,
+                                                         bool& done,
+                                                         SQLite::Statement& s,
+                                                         uint32_t maxResults)
+  {
+    target.clear();
+
+    while (target.size() < maxResults && s.Step())
+    {
+      int64_t seq = s.ColumnInt64(0);
+      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1));
+      std::string publicId = s.ColumnString(2);
+
+      ExportedResource resource(seq, 
+                                resourceType,
+                                publicId,
+                                s.ColumnString(3),  // modality
+                                s.ColumnString(8),  // date
+                                s.ColumnString(4),  // patient ID
+                                s.ColumnString(5),  // study instance UID
+                                s.ColumnString(6),  // series instance UID
+                                s.ColumnString(7)); // sop instance UID
+
+      target.push_back(resource);
+    }
+
+    done = !(target.size() == maxResults && s.Step());
+  }
+
+
+  void DatabaseWrapperBase::GetExportedResources(std::list<ExportedResource>& target,
+                                                 bool& done,
+                                                 int64_t since,
+                                                 uint32_t maxResults)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?");
+    s.BindInt64(0, since);
+    s.BindInt(1, maxResults + 1);
+    GetExportedResourcesInternal(target, done, s, maxResults);
+  }
+
+    
+  void DatabaseWrapperBase::GetLastExportedResource(std::list<ExportedResource>& target)
+  {
+    bool done;  // Ignored
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1");
+    GetExportedResourcesInternal(target, done, s, 1);
+  }
+
+
+    
+  uint64_t DatabaseWrapperBase::GetTotalCompressedSize()
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles");
+    s.Run();
+    return static_cast<uint64_t>(s.ColumnInt64(0));
+  }
+
+    
+  uint64_t DatabaseWrapperBase::GetTotalUncompressedSize()
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
+    s.Run();
+    return static_cast<uint64_t>(s.ColumnInt64(0));
+  }
+
+  void DatabaseWrapperBase::GetAllInternalIds(std::list<int64_t>& target,
+                                              ResourceType resourceType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId FROM Resources WHERE resourceType=?");
+    s.BindInt(0, resourceType);
+
+    target.clear();
+    while (s.Step())
+    {
+      target.push_back(s.ColumnInt64(0));
+    }
+  }
+
+
+  void DatabaseWrapperBase::GetAllPublicIds(std::list<std::string>& target,
+                                            ResourceType resourceType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?");
+    s.BindInt(0, resourceType);
+
+    target.clear();
+    while (s.Step())
+    {
+      target.push_back(s.ColumnString(0));
+    }
+  }
+
+  void DatabaseWrapperBase::GetAllPublicIds(std::list<std::string>& target,
+                                            ResourceType resourceType,
+                                            size_t since,
+                                            size_t limit)
+  {
+    if (limit == 0)
+    {
+      target.clear();
+      return;
+    }
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=? LIMIT ? OFFSET ?");
+    s.BindInt(0, resourceType);
+    s.BindInt64(1, limit);
+    s.BindInt64(2, since);
+
+    target.clear();
+    while (s.Step())
+    {
+      target.push_back(s.ColumnString(0));
+    }
+  }
+
+
+  uint64_t DatabaseWrapperBase::GetResourceCount(ResourceType resourceType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT COUNT(*) FROM Resources WHERE resourceType=?");
+    s.BindInt(0, resourceType);
+    
+    if (!s.Step())
+    {
+      return 0;
+    }
+    else
+    {
+      int64_t c = s.ColumnInt(0);
+      assert(!s.Step());
+      return c;
+    }
+  }
+
+
+  bool DatabaseWrapperBase::SelectPatientToRecycle(int64_t& internalId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                        "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1");
+   
+    if (!s.Step())
+    {
+      // No patient remaining or all the patients are protected
+      return false;
+    }
+    else
+    {
+      internalId = s.ColumnInt(0);
+      return true;
+    }    
+  }
+
+  bool DatabaseWrapperBase::SelectPatientToRecycle(int64_t& internalId,
+                                                   int64_t patientIdToAvoid)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                        "SELECT patientId FROM PatientRecyclingOrder "
+                        "WHERE patientId != ? ORDER BY seq ASC LIMIT 1");
+    s.BindInt64(0, patientIdToAvoid);
+
+    if (!s.Step())
+    {
+      // No patient remaining or all the patients are protected
+      return false;
+    }
+    else
+    {
+      internalId = s.ColumnInt(0);
+      return true;
+    }   
+  }
+
+  bool DatabaseWrapperBase::IsProtectedPatient(int64_t internalId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                        "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?");
+    s.BindInt64(0, internalId);
+    return !s.Step();
+  }
+
+  void DatabaseWrapperBase::SetProtectedPatient(int64_t internalId, 
+                                                bool isProtected)
+  {
+    if (isProtected)
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
+      s.BindInt64(0, internalId);
+      s.Run();
+    }
+    else if (IsProtectedPatient(internalId))
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
+      s.BindInt64(0, internalId);
+      s.Run();
+    }
+    else
+    {
+      // Nothing to do: The patient is already unprotected
+    }
+  }
+
+
+
+  bool DatabaseWrapperBase::IsExistingResource(int64_t internalId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT * FROM Resources WHERE internalId=?");
+    s.BindInt64(0, internalId);
+    return s.Step();
+  }
+
+
+
+  void DatabaseWrapperBase::LookupIdentifier(std::list<int64_t>& target,
+                                             ResourceType level,
+                                             const DicomTag& tag,
+                                             IdentifierConstraintType type,
+                                             const std::string& value)
+  {
+    static const char* COMMON = ("SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE "
+                                 "d.id = r.internalId AND r.resourceType=? AND "
+                                 "d.tagGroup=? AND d.tagElement=? AND ");
+
+    std::auto_ptr<SQLite::Statement> s;
+
+    switch (type)
+    {
+      case IdentifierConstraintType_GreaterOrEqual:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value>=?"));
+        break;
+
+      case IdentifierConstraintType_SmallerOrEqual:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value<=?"));
+        break;
+
+      case IdentifierConstraintType_Wildcard:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value GLOB ?"));
+        break;
+
+      case IdentifierConstraintType_Equal:
+      default:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value=?"));
+        break;
+    }
+
+    assert(s.get() != NULL);
+
+    s->BindInt(0, level);
+    s->BindInt(1, tag.GetGroup());
+    s->BindInt(2, tag.GetElement());
+    s->BindString(3, value);
+
+    target.clear();
+
+    while (s->Step())
+    {
+      target.push_back(s->ColumnInt64(0));
+    }    
+  }
+
+
+  void DatabaseWrapperBase::LookupIdentifierRange(std::list<int64_t>& target,
+                                                  ResourceType level,
+                                                  const DicomTag& tag,
+                                                  const std::string& start,
+                                                  const std::string& end)
+  {
+    SQLite::Statement statement(db_, SQLITE_FROM_HERE,
+                                "SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE "
+                                "d.id = r.internalId AND r.resourceType=? AND "
+                                "d.tagGroup=? AND d.tagElement=? AND d.value>=? AND d.value<=?");
+
+    statement.BindInt(0, level);
+    statement.BindInt(1, tag.GetGroup());
+    statement.BindInt(2, tag.GetElement());
+    statement.BindString(3, start);
+    statement.BindString(4, end);
+
+    target.clear();
+
+    while (statement.Step())
+    {
+      target.push_back(statement.ColumnInt64(0));
+    }    
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,210 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Core/DicomFormat/DicomMap.h"
+#include "../Core/DicomFormat/DicomTag.h"
+#include "../Core/Enumerations.h"
+#include "../Core/FileStorage/FileInfo.h"
+#include "../Core/SQLite/Connection.h"
+#include "../OrthancServer/ExportedResource.h"
+#include "../OrthancServer/ServerIndexChange.h"
+#include "ServerEnumerations.h"
+
+#include <list>
+
+
+namespace Orthanc
+{
+  /**
+   * This class is shared between the Orthanc core and the sample
+   * database plugin whose code is in
+   * "../Plugins/Samples/DatabasePlugin".
+   **/
+  class DatabaseWrapperBase
+  {
+  private:
+    SQLite::Connection&  db_;
+
+    ErrorCode GetChangesInternal(std::list<ServerIndexChange>& target,
+                                 bool& done,
+                                 SQLite::Statement& s,
+                                 uint32_t maxResults);
+
+    void GetExportedResourcesInternal(std::list<ExportedResource>& target,
+                                      bool& done,
+                                      SQLite::Statement& s,
+                                      uint32_t maxResults);
+
+  public:
+    DatabaseWrapperBase(SQLite::Connection& db) : db_(db)
+    {
+    }
+
+    void SetGlobalProperty(GlobalProperty property,
+                           const std::string& value);
+
+    bool LookupGlobalProperty(std::string& target,
+                              GlobalProperty property);
+
+    int64_t CreateResource(const std::string& publicId,
+                           ResourceType type);
+
+    bool LookupResource(int64_t& id,
+                        ResourceType& type,
+                        const std::string& publicId);
+
+    ErrorCode LookupParent(bool& found,
+                           int64_t& parentId,
+                           int64_t resourceId);
+
+    bool GetPublicId(std::string& result,
+                     int64_t resourceId);
+
+    ErrorCode GetResourceType(ResourceType& result,
+                              int64_t resourceId);
+
+    void AttachChild(int64_t parent,
+                     int64_t child);
+
+    void SetMetadata(int64_t id,
+                     MetadataType type,
+                     const std::string& value);
+
+    void DeleteMetadata(int64_t id,
+                        MetadataType type);
+
+    bool LookupMetadata(std::string& target,
+                        int64_t id,
+                        MetadataType type);
+
+    void ListAvailableMetadata(std::list<MetadataType>& target,
+                               int64_t id);
+
+    void AddAttachment(int64_t id,
+                       const FileInfo& attachment);
+
+    void DeleteAttachment(int64_t id,
+                          FileContentType attachment);
+
+    void ListAvailableAttachments(std::list<FileContentType>& target,
+                                  int64_t id);
+
+    bool LookupAttachment(FileInfo& attachment,
+                          int64_t id,
+                          FileContentType contentType);
+
+
+    void ClearMainDicomTags(int64_t id);
+
+
+    void SetMainDicomTag(int64_t id,
+                         const DicomTag& tag,
+                         const std::string& value);
+
+    void SetIdentifierTag(int64_t id,
+                          const DicomTag& tag,
+                          const std::string& value);
+
+    void GetMainDicomTags(DicomMap& map,
+                          int64_t id);
+
+    void GetChildrenPublicId(std::list<std::string>& target,
+                             int64_t id);
+
+    void GetChildrenInternalId(std::list<int64_t>& target,
+                               int64_t id);
+
+    void LogChange(int64_t internalId,
+                   const ServerIndexChange& change);
+
+    ErrorCode GetChanges(std::list<ServerIndexChange>& target,
+                         bool& done,
+                         int64_t since,
+                         uint32_t maxResults);
+
+    ErrorCode GetLastChange(std::list<ServerIndexChange>& target);
+
+    void LogExportedResource(const ExportedResource& resource);
+
+    void GetExportedResources(std::list<ExportedResource>& target,
+                              bool& done,
+                              int64_t since,
+                              uint32_t maxResults);
+    
+    void GetLastExportedResource(std::list<ExportedResource>& target);
+    
+    uint64_t GetTotalCompressedSize();
+    
+    uint64_t GetTotalUncompressedSize();
+
+    void GetAllInternalIds(std::list<int64_t>& target,
+                           ResourceType resourceType);
+
+    void GetAllPublicIds(std::list<std::string>& target,
+                         ResourceType resourceType);
+
+    void GetAllPublicIds(std::list<std::string>& target,
+                         ResourceType resourceType,
+                         size_t since,
+                         size_t limit);
+
+    uint64_t GetResourceCount(ResourceType resourceType);
+
+    bool SelectPatientToRecycle(int64_t& internalId);
+
+    bool SelectPatientToRecycle(int64_t& internalId,
+                                int64_t patientIdToAvoid);
+
+    bool IsProtectedPatient(int64_t internalId);
+
+    void SetProtectedPatient(int64_t internalId, 
+                             bool isProtected);
+
+    bool IsExistingResource(int64_t internalId);
+
+    void LookupIdentifier(std::list<int64_t>& result,
+                          ResourceType level,
+                          const DicomTag& tag,
+                          IdentifierConstraintType type,
+                          const std::string& value);
+
+    void LookupIdentifierRange(std::list<int64_t>& result,
+                               ResourceType level,
+                               const DicomTag& tag,
+                               const std::string& start,
+                               const std::string& end);
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/DatabasePluginSample/Plugin.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,102 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "Database.h"
+
+#include <memory>
+#include <iostream>
+#include <boost/algorithm/string/predicate.hpp>
+
+static OrthancPluginContext*  context_ = NULL;
+static std::auto_ptr<OrthancPlugins::IDatabaseBackend>  backend_;
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    context_ = c;
+    OrthancPluginLogWarning(context_, "Sample plugin is initializing");
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(c) == 0)
+    {
+      char info[256];
+      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
+              c->orthancVersion,
+              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      OrthancPluginLogError(context_, info);
+      return -1;
+    }
+
+    std::string path = "SampleDatabase.sqlite";
+    uint32_t argCount = OrthancPluginGetCommandLineArgumentsCount(context_);
+    for (uint32_t i = 0; i < argCount; i++)
+    {
+      char* tmp = OrthancPluginGetCommandLineArgument(context_, i);
+      std::string argument(tmp);
+      OrthancPluginFreeString(context_, tmp);
+
+      if (boost::starts_with(argument, "--database="))
+      {
+        path = argument.substr(11);
+      }
+    }
+
+    std::string s = "Using the following SQLite database: " + path;
+    OrthancPluginLogWarning(context_, s.c_str());
+
+    backend_.reset(new Database(path));
+    OrthancPlugins::DatabaseBackendAdapter::Register(context_, *backend_);
+
+    return 0;
+  }
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    backend_.reset(NULL);
+  }
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "sample-database";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return "1.0";
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/BagOfTasks.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,84 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../ICommand.h"
+
+#include <list>
+#include <cstddef>
+
+namespace Orthanc
+{
+  class BagOfTasks : public boost::noncopyable
+  {
+  private:
+    typedef std::list<ICommand*>  Tasks;
+
+    Tasks  tasks_;
+
+  public:
+    ~BagOfTasks()
+    {
+      for (Tasks::iterator it = tasks_.begin(); it != tasks_.end(); ++it)
+      {
+        delete *it;
+      }
+    }
+
+    ICommand* Pop()
+    {
+      ICommand* task = tasks_.front();
+      tasks_.pop_front();
+      return task;
+    }
+
+    void Push(ICommand* task)   // Takes ownership
+    {
+      if (task != NULL)
+      {
+        tasks_.push_back(task);
+      }
+    }
+
+    size_t GetSize() const
+    {
+      return tasks_.size();
+    }
+
+    bool IsEmpty() const
+    {
+      return tasks_.empty();
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/BagOfTasksProcessor.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,277 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "BagOfTasksProcessor.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+#include <stdio.h>
+
+namespace Orthanc
+{
+  class BagOfTasksProcessor::Task : public IDynamicObject
+  {
+  private:
+    uint64_t                 bag_;
+    std::auto_ptr<ICommand>  command_;
+
+  public:
+    Task(uint64_t  bag,
+         ICommand* command) :
+      bag_(bag),
+      command_(command)
+    {
+    }
+
+    bool Execute()
+    {
+      try
+      {
+        return command_->Execute();
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Exception while processing a bag of tasks: " << e.What();
+        return false;
+      }
+      catch (std::runtime_error& e)
+      {
+        LOG(ERROR) << "Runtime exception while processing a bag of tasks: " << e.what();
+        return false;
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Native exception while processing a bag of tasks";
+        return false;
+      }
+    }
+
+    uint64_t GetBag()
+    {
+      return bag_;
+    }
+  };
+
+
+  void BagOfTasksProcessor::SignalProgress(Task& task,
+                                           Bag& bag)
+  {
+    assert(bag.done_ < bag.size_);
+
+    bag.done_ += 1;
+
+    if (bag.done_ == bag.size_)
+    {
+      exitStatus_[task.GetBag()] = (bag.status_ == BagStatus_Running);
+      bagFinished_.notify_all();
+    }
+  }
+
+  void BagOfTasksProcessor::Worker(BagOfTasksProcessor* that)
+  {
+    while (that->continue_)
+    {
+      std::auto_ptr<IDynamicObject> obj(that->queue_.Dequeue(100));
+      if (obj.get() != NULL)
+      {
+        Task& task = *dynamic_cast<Task*>(obj.get());
+
+        {
+          boost::mutex::scoped_lock lock(that->mutex_);
+
+          Bags::iterator bag = that->bags_.find(task.GetBag());
+          assert(bag != that->bags_.end());
+          assert(bag->second.done_ < bag->second.size_);
+
+          if (bag->second.status_ != BagStatus_Running)
+          {
+            // Do not execute this task, as its parent bag of tasks
+            // has failed or is tagged as canceled
+            that->SignalProgress(task, bag->second);
+            continue;
+          }
+        }
+
+        bool success = task.Execute();
+
+        {
+          boost::mutex::scoped_lock lock(that->mutex_);
+
+          Bags::iterator bag = that->bags_.find(task.GetBag());
+          assert(bag != that->bags_.end());
+
+          if (!success)
+          {
+            bag->second.status_ = BagStatus_Failed;
+          }
+
+          that->SignalProgress(task, bag->second);
+        }
+      }
+    }
+  }
+
+
+  void BagOfTasksProcessor::Cancel(int64_t bag)
+  {
+    boost::mutex::scoped_lock  lock(mutex_);
+
+    Bags::iterator it = bags_.find(bag);
+    if (it != bags_.end())
+    {
+      it->second.status_ = BagStatus_Canceled;
+    }
+  }
+
+
+  bool BagOfTasksProcessor::Join(int64_t bag)
+  {
+    boost::mutex::scoped_lock  lock(mutex_);
+
+    while (continue_)
+    {
+      ExitStatus::iterator it = exitStatus_.find(bag);
+      if (it == exitStatus_.end())  // The bag is still running
+      {
+        bagFinished_.wait(lock);
+      }
+      else
+      {
+        bool status = it->second;
+        exitStatus_.erase(it);
+        return status;
+      }
+    }
+
+    return false;   // The processor is stopping
+  }
+
+
+  float BagOfTasksProcessor::GetProgress(int64_t bag)
+  {
+    boost::mutex::scoped_lock  lock(mutex_);
+
+    Bags::const_iterator it = bags_.find(bag);
+    if (it == bags_.end())
+    {
+      // The bag of tasks has finished
+      return 1.0f;
+    }
+    else
+    {
+      return (static_cast<float>(it->second.done_) / 
+              static_cast<float>(it->second.size_));
+    }
+  }
+
+
+  bool BagOfTasksProcessor::Handle::Join()
+  {
+    if (hasJoined_)
+    {
+      return status_;
+    }
+    else
+    {
+      status_ = that_.Join(bag_);
+      hasJoined_ = true;
+      return status_;
+    }
+  }
+
+
+  BagOfTasksProcessor::BagOfTasksProcessor(size_t countThreads) : 
+    countBags_(0),
+    continue_(true)
+  {
+    if (countThreads == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    threads_.resize(countThreads);
+
+    for (size_t i = 0; i < threads_.size(); i++)
+    {
+      threads_[i] = new boost::thread(Worker, this);
+    }
+  }
+
+
+  BagOfTasksProcessor::~BagOfTasksProcessor()
+  {
+    continue_ = false;
+
+    bagFinished_.notify_all();   // Wakes up all the pending "Join()"
+
+    for (size_t i = 0; i < threads_.size(); i++)
+    {
+      if (threads_[i])
+      {
+        if (threads_[i]->joinable())
+        {
+          threads_[i]->join();
+        }
+
+        delete threads_[i];
+        threads_[i] = NULL;
+      }
+    }
+  }
+
+
+  BagOfTasksProcessor::Handle* BagOfTasksProcessor::Submit(BagOfTasks& tasks)
+  {
+    if (tasks.GetSize() == 0)
+    {
+      return new Handle(*this, 0, true);
+    }
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    uint64_t id = countBags_;
+    countBags_ += 1;
+
+    Bag bag(tasks.GetSize());
+    bags_[id] = bag;
+
+    while (!tasks.IsEmpty())
+    {
+      queue_.Enqueue(new Task(id, tasks.Pop()));
+    }
+
+    return new Handle(*this, id, false);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/BagOfTasksProcessor.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "BagOfTasks.h"
+#include "SharedMessageQueue.h"
+
+#include <stdint.h>
+#include <map>
+
+namespace Orthanc
+{
+  class BagOfTasksProcessor : public boost::noncopyable
+  {
+  private:
+    enum BagStatus
+    {
+      BagStatus_Running,
+      BagStatus_Canceled,
+      BagStatus_Failed
+    };
+
+
+    struct Bag
+    {
+      size_t    size_;
+      size_t    done_;
+      BagStatus status_;
+
+      Bag() :
+        size_(0),
+        done_(0),
+        status_(BagStatus_Failed)
+      {
+      }
+
+      explicit Bag(size_t size) : 
+        size_(size),
+        done_(0),
+        status_(BagStatus_Running)
+      {
+      }
+    };
+
+    class Task;
+
+
+    typedef std::map<uint64_t, Bag>   Bags;
+    typedef std::map<uint64_t, bool>  ExitStatus;
+
+    SharedMessageQueue  queue_;
+
+    boost::mutex  mutex_;
+    uint64_t  countBags_;
+    Bags bags_;
+    std::vector<boost::thread*>   threads_;
+    ExitStatus  exitStatus_;
+    bool continue_;
+
+    boost::condition_variable  bagFinished_;
+
+    static void Worker(BagOfTasksProcessor* that);
+
+    void Cancel(int64_t bag);
+
+    bool Join(int64_t bag);
+
+    float GetProgress(int64_t bag);
+
+    void SignalProgress(Task& task,
+                        Bag& bag);
+
+  public:
+    class Handle : public boost::noncopyable
+    {
+      friend class BagOfTasksProcessor;
+
+    private:
+      BagOfTasksProcessor&  that_;
+      uint64_t              bag_;
+      bool                  hasJoined_;
+      bool                  status_;
+ 
+      Handle(BagOfTasksProcessor&  that,
+             uint64_t bag,
+             bool empty) : 
+        that_(that),
+        bag_(bag),
+        hasJoined_(empty)
+      {
+      }
+
+    public:
+      ~Handle()
+      {
+        Join();
+      }
+
+      void Cancel()
+      {
+        that_.Cancel(bag_);
+      }
+
+      bool Join();
+
+      float GetProgress()
+      {
+        return that_.GetProgress(bag_);
+      }
+    };
+  
+
+    explicit BagOfTasksProcessor(size_t countThreads);
+
+    ~BagOfTasksProcessor();
+
+    Handle* Submit(BagOfTasks& tasks);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/ICommand.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,49 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IDynamicObject.h"
+
+namespace Orthanc
+{
+  /**
+   * This class is the base class for the "Command" design pattern.
+   * http://en.wikipedia.org/wiki/Command_pattern
+   **/
+  class ICommand : public IDynamicObject
+  {
+  public:
+    virtual bool Execute() = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/ILockable.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,54 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class ILockable : public boost::noncopyable
+  {
+    friend class Locker;
+
+  protected:
+    virtual void Lock() = 0;
+
+    virtual void Unlock() = 0;
+
+  public:
+    virtual ~ILockable()
+    {
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/Locker.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,56 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ILockable.h"
+
+namespace Orthanc
+{
+  class Locker : public boost::noncopyable
+  {
+  private:
+    ILockable& lockable_;
+
+  public:
+    Locker(ILockable& lockable) : lockable_(lockable)
+    {
+      lockable_.Lock();
+    }
+
+    virtual ~Locker()
+    {
+      lockable_.Unlock();
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/Mutex.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,122 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "Mutex.h"
+
+#include "../OrthancException.h"
+
+#if defined(_WIN32)
+#include <windows.h>
+#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
+#include <pthread.h>
+#else
+#error Support your platform here
+#endif
+
+namespace Orthanc
+{
+#if defined (_WIN32)
+
+  struct Mutex::PImpl
+  {
+    CRITICAL_SECTION criticalSection_;
+  };
+
+  Mutex::Mutex()
+  {
+    pimpl_ = new PImpl;
+    ::InitializeCriticalSection(&pimpl_->criticalSection_);
+  }
+
+  Mutex::~Mutex()
+  {
+    ::DeleteCriticalSection(&pimpl_->criticalSection_);
+    delete pimpl_;
+  }
+
+  void Mutex::Lock()
+  {
+    ::EnterCriticalSection(&pimpl_->criticalSection_);
+  }
+
+  void Mutex::Unlock()
+  {
+    ::LeaveCriticalSection(&pimpl_->criticalSection_);
+  }
+
+
+#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
+
+  struct Mutex::PImpl
+  {
+    pthread_mutex_t mutex_;
+  };
+
+  Mutex::Mutex()
+  {
+    pimpl_ = new PImpl;
+
+    if (pthread_mutex_init(&pimpl_->mutex_, NULL) != 0)
+    {
+      delete pimpl_;
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+  Mutex::~Mutex()
+  {
+    pthread_mutex_destroy(&pimpl_->mutex_);
+    delete pimpl_;
+  }
+
+  void Mutex::Lock()
+  {
+    if (pthread_mutex_lock(&pimpl_->mutex_) != 0)
+    {
+      throw OrthancException(ErrorCode_InternalError);    
+    }
+  }
+
+  void Mutex::Unlock()
+  {
+    if (pthread_mutex_unlock(&pimpl_->mutex_) != 0)
+    {
+      throw OrthancException(ErrorCode_InternalError);    
+    }
+  }
+
+#else
+#error Support your plateform here
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/Mutex.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ILockable.h"
+
+namespace Orthanc
+{
+  class Mutex : public ILockable
+  {
+  private:
+    struct PImpl;
+
+    PImpl *pimpl_;
+
+  protected:
+    virtual void Lock();
+
+    virtual void Unlock();
+    
+  public:
+    Mutex();
+
+    ~Mutex();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/ReaderWriterLock.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,126 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "ReaderWriterLock.h"
+
+#include <boost/thread/shared_mutex.hpp>
+
+namespace Orthanc
+{
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation
+    // modules.
+
+    class ReaderLockable : public ILockable
+    {
+    private:
+      boost::shared_mutex& lock_;
+
+    protected:
+      virtual void Lock()
+      {
+        lock_.lock_shared();
+      }
+
+      virtual void Unlock()
+      {
+        lock_.unlock_shared();        
+      }
+
+    public:
+      explicit ReaderLockable(boost::shared_mutex& lock) : lock_(lock)
+      {
+      }
+    };
+
+
+    class WriterLockable : public ILockable
+    {
+    private:
+      boost::shared_mutex& lock_;
+
+    protected:
+      virtual void Lock()
+      {
+        lock_.lock();
+      }
+
+      virtual void Unlock()
+      {
+        lock_.unlock();        
+      }
+
+    public:
+      explicit WriterLockable(boost::shared_mutex& lock) : lock_(lock)
+      {
+      }
+    };
+  }
+
+  struct ReaderWriterLock::PImpl
+  {
+    boost::shared_mutex lock_;
+    ReaderLockable reader_;
+    WriterLockable writer_;
+
+    PImpl() : reader_(lock_), writer_(lock_)
+    {
+    }
+  };
+
+
+  ReaderWriterLock::ReaderWriterLock()
+  {
+    pimpl_ = new PImpl;
+  }
+
+
+  ReaderWriterLock::~ReaderWriterLock()
+  {
+    delete pimpl_;
+  }
+
+
+  ILockable&  ReaderWriterLock::ForReader()
+  {
+    return pimpl_->reader_;
+  }
+
+
+  ILockable&  ReaderWriterLock::ForWriter()
+  {
+    return pimpl_->writer_;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/ReaderWriterLock.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ILockable.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class ReaderWriterLock : public boost::noncopyable
+  {
+  private:
+    struct PImpl;
+
+    PImpl *pimpl_;
+
+  public:
+    ReaderWriterLock();
+
+    virtual ~ReaderWriterLock();
+
+    ILockable& ForReader();
+
+    ILockable& ForWriter();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/CallSystemCommand.cpp	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "CallSystemCommand.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/Toolbox.h"
+#include "../../Core/TemporaryFile.h"
+
+namespace Orthanc
+{
+  CallSystemCommand::CallSystemCommand(ServerContext& context,
+                                       const std::string& command,
+                                       const std::vector<std::string>& arguments) : 
+    context_(context),
+    command_(command),
+    arguments_(arguments)
+  {
+  }
+
+  bool CallSystemCommand::Apply(ListOfStrings& outputs,
+                                const ListOfStrings& inputs)
+  {
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Calling system command " << command_ << " on instance " << *it;
+
+      try
+      {
+        std::string dicom;
+        context_.ReadDicom(dicom, *it);
+
+        TemporaryFile tmp;
+        tmp.Write(dicom);
+
+        std::vector<std::string> args = arguments_;
+        args.push_back(tmp.GetPath());
+
+        SystemToolbox::ExecuteSystemCommand(command_, args);
+
+        // Only chain with other commands if this command succeeds
+        outputs.push_back(*it);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Unable to call system command " << command_ 
+                   << " on instance " << *it << " in a Lua script: " << e.What();
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/CallSystemCommand.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,56 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IServerCommand.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class CallSystemCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+    std::string command_;
+    std::vector<std::string> arguments_;
+
+  public:
+    CallSystemCommand(ServerContext& context,
+                      const std::string& command,
+                      const std::vector<std::string>& arguments);
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/DeleteInstanceCommand.cpp	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "DeleteInstanceCommand.h"
+
+#include "../../Core/Logging.h"
+
+namespace Orthanc
+{
+  bool DeleteInstanceCommand::Apply(ListOfStrings& outputs,
+                                    const ListOfStrings& inputs)
+  {
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Deleting instance " << *it;
+
+      try
+      {
+        Json::Value tmp;
+        context_.DeleteResource(tmp, *it, ResourceType_Instance);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Unable to delete instance " << *it << ": " << e.What();
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/DeleteInstanceCommand.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,54 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IServerCommand.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class DeleteInstanceCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+
+  public:
+    DeleteInstanceCommand(ServerContext& context) : context_(context)
+    {
+    }
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/IServerCommand.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,54 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <list>
+#include <string>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class IServerCommand : public boost::noncopyable
+  {
+  public:
+    typedef std::list<std::string>  ListOfStrings;
+
+    virtual ~IServerCommand()
+    {
+    }
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ModifyInstanceCommand.cpp	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "ModifyInstanceCommand.h"
+
+#include "../../Core/Logging.h"
+
+namespace Orthanc
+{
+  ModifyInstanceCommand::ModifyInstanceCommand(ServerContext& context,
+                                               RequestOrigin origin,
+                                               DicomModification* modification) :
+    context_(context),
+    origin_(origin),
+    modification_(modification)
+  {
+    modification_->SetAllowManualIdentifiers(true);
+
+    if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID))
+    {
+      modification_->SetLevel(ResourceType_Patient);
+    }
+    else if (modification_->IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+    {
+      modification_->SetLevel(ResourceType_Study);
+    }
+    else if (modification_->IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+    {
+      modification_->SetLevel(ResourceType_Series);
+    }
+    else
+    {
+      modification_->SetLevel(ResourceType_Instance);
+    }
+
+    if (origin_ != RequestOrigin_Lua)
+    {
+      // TODO If issued from HTTP, "remoteIp" and "username" must be provided
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  ModifyInstanceCommand::~ModifyInstanceCommand()
+  {
+    if (modification_)
+    {
+      delete modification_;
+    }
+  }
+
+
+  bool ModifyInstanceCommand::Apply(ListOfStrings& outputs,
+                                    const ListOfStrings& inputs)
+  {
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Modifying resource " << *it;
+
+      try
+      {
+        std::auto_ptr<ParsedDicomFile> modified;
+
+        {
+          ServerContext::DicomCacheLocker lock(context_, *it);
+          modified.reset(lock.GetDicom().Clone(true));
+        }
+
+        modification_->Apply(*modified);
+
+        DicomInstanceToStore toStore;
+        assert(origin_ == RequestOrigin_Lua);
+        toStore.SetLuaOrigin();
+        toStore.SetParsedDicomFile(*modified);
+        // TODO other metadata
+        toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, *it);
+
+        std::string modifiedId;
+        context_.Store(modifiedId, toStore);
+
+        // Only chain with other commands if this command succeeds
+        outputs.push_back(modifiedId);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Unable to modify instance " << *it << ": " << e.What();
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ModifyInstanceCommand.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,64 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IServerCommand.h"
+#include "../ServerContext.h"
+#include "../../Core/DicomParsing/DicomModification.h"
+
+namespace Orthanc
+{
+  class ModifyInstanceCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+    RequestOrigin origin_;
+    DicomModification* modification_;
+
+  public:
+    ModifyInstanceCommand(ServerContext& context,
+                          RequestOrigin origin,
+                          DicomModification* modification);  // takes the ownership
+
+    virtual ~ModifyInstanceCommand();
+
+    const DicomModification& GetModification() const
+    {
+      return *modification_;
+    }
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,188 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "ReusableDicomUserConnection.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  static boost::posix_time::ptime Now()
+  {
+    return boost::posix_time::microsec_clock::local_time();
+  }
+
+  void ReusableDicomUserConnection::Open(const std::string& localAet,
+                                         const RemoteModalityParameters& remote)
+  {
+    if (connection_ != NULL &&
+        connection_->GetLocalApplicationEntityTitle() == localAet &&
+        connection_->GetRemoteApplicationEntityTitle() == remote.GetApplicationEntityTitle() &&
+        connection_->GetRemoteHost() == remote.GetHost() &&
+        connection_->GetRemotePort() == remote.GetPort() &&
+        connection_->GetRemoteManufacturer() == remote.GetManufacturer())
+    {
+      // The current connection can be reused
+      LOG(INFO) << "Reusing the previous SCU connection";
+      return;
+    }
+
+    Close();
+
+    connection_ = new DicomUserConnection();
+    connection_->SetLocalApplicationEntityTitle(localAet);
+    connection_->SetRemoteModality(remote);
+    connection_->Open();
+  }
+    
+  void ReusableDicomUserConnection::Close()
+  {
+    if (connection_ != NULL)
+    {
+      delete connection_;
+      connection_ = NULL;
+    }
+  }
+
+  void ReusableDicomUserConnection::CloseThread(ReusableDicomUserConnection* that)
+  {
+    for (;;)
+    {
+      boost::this_thread::sleep(boost::posix_time::milliseconds(100));
+      if (!that->continue_)
+      {
+        //LOG(INFO) << "Finishing the thread watching the global SCU connection";
+        return;
+      }
+
+      {
+        boost::mutex::scoped_lock lock(that->mutex_);
+        if (that->connection_ != NULL &&
+            Now() >= that->lastUse_ + that->timeBeforeClose_)
+        {
+          LOG(INFO) << "Closing the global SCU connection after timeout";
+          that->Close();
+        }
+      }
+    }
+  }
+    
+
+  ReusableDicomUserConnection::Locker::Locker(ReusableDicomUserConnection& that,
+                                              const std::string& localAet,
+                                              const RemoteModalityParameters& remote) :
+    ::Orthanc::Locker(that)
+  {
+    that.Open(localAet, remote);
+    connection_ = that.connection_;    
+  }
+
+
+  DicomUserConnection& ReusableDicomUserConnection::Locker::GetConnection()
+  {
+    if (connection_ == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    return *connection_;
+  }      
+
+  ReusableDicomUserConnection::ReusableDicomUserConnection() : 
+    connection_(NULL), 
+    timeBeforeClose_(boost::posix_time::seconds(5))  // By default, close connection after 5 seconds
+  {
+    lastUse_ = Now();
+    continue_ = true;
+    closeThread_ = boost::thread(CloseThread, this);
+  }
+
+  ReusableDicomUserConnection::~ReusableDicomUserConnection()
+  {
+    if (continue_)
+    {
+      LOG(ERROR) << "INTERNAL ERROR: ReusableDicomUserConnection::Finalize() should be invoked manually to avoid mess in the destruction order!";
+      Finalize();
+    }
+  }
+
+  void ReusableDicomUserConnection::SetMillisecondsBeforeClose(uint64_t ms)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (ms == 0)
+    {
+      ms = 1;
+    }
+
+    timeBeforeClose_ = boost::posix_time::milliseconds(ms);
+  }
+
+  void ReusableDicomUserConnection::Lock()
+  {
+    mutex_.lock();
+  }
+
+  void ReusableDicomUserConnection::Unlock()
+  {
+    if (connection_ != NULL &&
+        connection_->GetRemoteManufacturer() == ModalityManufacturer_StoreScp)
+    {
+      // "storescp" from DCMTK has problems when reusing a
+      // connection. Always close.
+      Close();
+    }
+
+    lastUse_ = Now();
+    mutex_.unlock();
+  }
+
+  
+  void ReusableDicomUserConnection::Finalize()
+  {
+    if (continue_)
+    {
+      continue_ = false;
+
+      if (closeThread_.joinable())
+      {
+        closeThread_.join();
+      }
+
+      Close();
+    }
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,89 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomUserConnection.h"
+#include "../../Core/MultiThreading/Locker.h"
+
+#include <boost/thread.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+namespace Orthanc
+{
+  class ReusableDicomUserConnection : public ILockable
+  {
+  private:
+    boost::mutex mutex_;
+    DicomUserConnection* connection_;
+    bool continue_;
+    boost::posix_time::time_duration timeBeforeClose_;
+    boost::posix_time::ptime lastUse_;
+    boost::thread closeThread_;
+
+    void Open(const std::string& localAet,
+              const RemoteModalityParameters& remote);
+    
+    void Close();
+
+    static void CloseThread(ReusableDicomUserConnection* that);
+
+  protected:
+    virtual void Lock();
+
+    virtual void Unlock();
+    
+  public:
+    class Locker : public ::Orthanc::Locker
+    {
+    private:
+      DicomUserConnection* connection_;
+
+    public:
+      Locker(ReusableDicomUserConnection& that,
+             const std::string& localAet,
+             const RemoteModalityParameters& remote);
+
+      DicomUserConnection& GetConnection();
+    };
+
+    ReusableDicomUserConnection();
+
+    virtual ~ReusableDicomUserConnection();
+
+    void SetMillisecondsBeforeClose(uint64_t ms);
+
+    void Finalize();
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ServerCommandInstance.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,99 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "ServerCommandInstance.h"
+
+#include "../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  bool ServerCommandInstance::Execute(IListener& listener)
+  {
+    ListOfStrings outputs;
+
+    bool success = false;
+
+    try
+    {
+      if (command_->Apply(outputs, inputs_))
+      {
+        success = true;
+      }
+    }
+    catch (OrthancException&)
+    {
+    }
+
+    if (!success)
+    {
+      listener.SignalFailure(jobId_);
+      return true;
+    }
+
+    for (std::list<ServerCommandInstance*>::iterator
+           it = next_.begin(); it != next_.end(); ++it)
+    {
+      for (ListOfStrings::const_iterator
+             output = outputs.begin(); output != outputs.end(); ++output)
+      {
+        (*it)->AddInput(*output);
+      }
+    }
+
+    listener.SignalSuccess(jobId_);
+    return true;
+  }
+
+
+  ServerCommandInstance::ServerCommandInstance(IServerCommand *command,
+                                               const std::string& jobId) : 
+    command_(command), 
+    jobId_(jobId),
+    connectedToSink_(false)
+  {
+    if (command_ == NULL)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  ServerCommandInstance::~ServerCommandInstance()
+  {
+    if (command_ != NULL)
+    {
+      delete command_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ServerCommandInstance.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,105 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/IDynamicObject.h"
+#include "IServerCommand.h"
+
+namespace Orthanc
+{
+  class ServerCommandInstance : public IDynamicObject
+  {
+    friend class ServerScheduler;
+
+  public:
+    class IListener
+    {
+    public:
+      virtual ~IListener()
+      {
+      }
+
+      virtual void SignalSuccess(const std::string& jobId) = 0;
+
+      virtual void SignalFailure(const std::string& jobId) = 0;
+    };
+
+  private:
+    typedef IServerCommand::ListOfStrings  ListOfStrings;
+
+    IServerCommand *command_;
+    std::string jobId_;
+    ListOfStrings inputs_;
+    std::list<ServerCommandInstance*> next_;
+    bool connectedToSink_;
+
+    bool Execute(IListener& listener);
+
+  public:
+    ServerCommandInstance(IServerCommand *command,
+                          const std::string& jobId);
+
+    virtual ~ServerCommandInstance();
+
+    const std::string& GetJobId() const
+    {
+      return jobId_;
+    }
+
+    void AddInput(const std::string& input)
+    {
+      inputs_.push_back(input);
+    }
+
+    void ConnectOutput(ServerCommandInstance& next)
+    {
+      next_.push_back(&next);
+    }
+
+    void SetConnectedToSink(bool connected = true)
+    {
+      connectedToSink_ = connected;
+    }
+
+    bool IsConnectedToSink() const
+    {
+      return connectedToSink_;
+    }
+
+    const std::list<ServerCommandInstance*>& GetNextCommands() const
+    {
+      return next_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ServerJob.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,147 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "ServerJob.h"
+
+#include "../../Core/OrthancException.h"
+#include "../../Core/Toolbox.h"
+
+namespace Orthanc
+{
+  void ServerJob::CheckOrdering()
+  {
+    std::map<ServerCommandInstance*, unsigned int> index;
+
+    unsigned int count = 0;
+    for (std::list<ServerCommandInstance*>::const_iterator
+           it = filters_.begin(); it != filters_.end(); ++it)
+    {
+      index[*it] = count++;
+    }
+
+    for (std::list<ServerCommandInstance*>::const_iterator
+           it = filters_.begin(); it != filters_.end(); ++it)
+    {
+      const std::list<ServerCommandInstance*>& nextCommands = (*it)->GetNextCommands();
+
+      for (std::list<ServerCommandInstance*>::const_iterator
+             next = nextCommands.begin(); next != nextCommands.end(); ++next)
+      {
+        if (index.find(*next) == index.end() ||
+            index[*next] <= index[*it])
+        {
+          // You must reorder your calls to "ServerJob::AddCommand"
+          throw OrthancException(ErrorCode_BadJobOrdering);
+        }
+      }
+    }
+  }
+
+
+  size_t ServerJob::Submit(SharedMessageQueue& target,
+                           ServerCommandInstance::IListener& listener)
+  {
+    if (submitted_)
+    {
+      // This job has already been submitted
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    CheckOrdering();
+
+    size_t size = filters_.size();
+
+    for (std::list<ServerCommandInstance*>::iterator 
+           it = filters_.begin(); it != filters_.end(); ++it)
+    {
+      target.Enqueue(*it);
+    }
+
+    filters_.clear();
+    submitted_ = true;
+
+    return size;
+  }
+
+
+  ServerJob::ServerJob() :
+    jobId_(Toolbox::GenerateUuid()),
+    submitted_(false),
+    description_("no description")
+  {
+  }
+
+
+  ServerJob::~ServerJob()
+  {
+    for (std::list<ServerCommandInstance*>::iterator
+           it = filters_.begin(); it != filters_.end(); ++it)
+    {
+      delete *it;
+    }
+
+    for (std::list<IDynamicObject*>::iterator
+           it = payloads_.begin(); it != payloads_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+
+  ServerCommandInstance& ServerJob::AddCommand(IServerCommand* filter)
+  {
+    if (submitted_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    filters_.push_back(new ServerCommandInstance(filter, jobId_));
+      
+    return *filters_.back();
+  }
+
+
+  IDynamicObject& ServerJob::AddPayload(IDynamicObject* payload)
+  {
+    if (submitted_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    payloads_.push_back(payload);
+      
+    return *filters_.back();
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ServerJob.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ServerCommandInstance.h"
+#include "../../Core/MultiThreading/SharedMessageQueue.h"
+
+namespace Orthanc
+{
+  class ServerJob
+  {
+    friend class ServerScheduler;
+
+  private:
+    std::list<ServerCommandInstance*> filters_;
+    std::list<IDynamicObject*> payloads_;
+    std::string jobId_;
+    bool submitted_;
+    std::string description_;
+
+    void CheckOrdering();
+
+    size_t Submit(SharedMessageQueue& target,
+                  ServerCommandInstance::IListener& listener);
+
+  public:
+    ServerJob();
+
+    ~ServerJob();
+
+    const std::string& GetId() const
+    {
+      return jobId_;
+    }
+
+    void SetDescription(const std::string& description)
+    {
+      description_ = description;
+    }
+
+    const std::string& GetDescription() const
+    {
+      return description_;
+    }
+
+    ServerCommandInstance& AddCommand(IServerCommand* filter);
+
+    // Take the ownership of a payload to a job. This payload will be
+    // automatically freed when the job succeeds or fails.
+    IDynamicObject& AddPayload(IDynamicObject* payload);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ServerScheduler.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,359 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "ServerScheduler.h"
+
+#include "../../Core/OrthancException.h"
+#include "../../Core/Logging.h"
+
+namespace Orthanc
+{
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+    class Sink : public IServerCommand
+    {
+    private:
+      ListOfStrings& target_;
+
+    public:
+      explicit Sink(ListOfStrings& target) : target_(target)
+      {
+      }
+
+      virtual bool Apply(ListOfStrings& outputs,
+                         const ListOfStrings& inputs)
+      {
+        for (ListOfStrings::const_iterator 
+               it = inputs.begin(); it != inputs.end(); ++it)
+        {
+          target_.push_back(*it);
+        }
+
+        return true;
+      }    
+    };
+  }
+
+
+  ServerScheduler::JobInfo& ServerScheduler::GetJobInfo(const std::string& jobId)
+  {
+    Jobs::iterator info = jobs_.find(jobId);
+
+    if (info == jobs_.end())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    return info->second;
+  }
+
+
+  void ServerScheduler::SignalSuccess(const std::string& jobId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    JobInfo& info = GetJobInfo(jobId);
+    info.success_++;
+
+    assert(info.failures_ == 0);
+
+    if (info.success_ >= info.size_)
+    {
+      if (info.watched_)
+      {
+        watchedJobStatus_[jobId] = JobStatus_Success;
+        watchedJobFinished_.notify_all();
+      }
+
+      LOG(INFO) << "Job successfully finished (" << info.description_ << ")";
+      jobs_.erase(jobId);
+
+      availableJob_.Release();
+    }
+  }
+
+
+  void ServerScheduler::SignalFailure(const std::string& jobId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    JobInfo& info = GetJobInfo(jobId);
+    info.failures_++;
+
+    if (info.success_ + info.failures_ >= info.size_)
+    {
+      if (info.watched_)
+      {
+        watchedJobStatus_[jobId] = JobStatus_Failure;
+        watchedJobFinished_.notify_all();
+      }
+
+      LOG(ERROR) << "Job has failed (" << info.description_ << ")";
+      jobs_.erase(jobId);
+
+      availableJob_.Release();
+    }
+  }
+
+
+  void ServerScheduler::Worker(ServerScheduler* that)
+  {
+    static const int32_t TIMEOUT = 100;
+
+    LOG(WARNING) << "The server scheduler has started";
+
+    while (!that->finish_)
+    {
+      std::auto_ptr<IDynamicObject> object(that->queue_.Dequeue(TIMEOUT));
+      if (object.get() != NULL)
+      {
+        ServerCommandInstance& filter = dynamic_cast<ServerCommandInstance&>(*object);
+
+        // Skip the execution of this filter if its parent job has
+        // previously failed.
+        bool jobHasFailed;
+        {
+          boost::mutex::scoped_lock lock(that->mutex_);
+          JobInfo& info = that->GetJobInfo(filter.GetJobId());
+          jobHasFailed = (info.failures_ > 0 || info.cancel_); 
+        }
+
+        if (jobHasFailed)
+        {
+          that->SignalFailure(filter.GetJobId());
+        }
+        else
+        {
+          filter.Execute(*that);
+        }
+      }
+    }
+  }
+
+
+  void ServerScheduler::SubmitInternal(ServerJob& job,
+                                       bool watched)
+  {
+    availableJob_.Acquire();
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    JobInfo info;
+    info.size_ = job.Submit(queue_, *this);
+    info.cancel_ = false;
+    info.success_ = 0;
+    info.failures_ = 0;
+    info.description_ = job.GetDescription();
+    info.watched_ = watched;
+
+    assert(info.size_ > 0);
+
+    if (watched)
+    {
+      watchedJobStatus_[job.GetId()] = JobStatus_Running;
+    }
+
+    jobs_[job.GetId()] = info;
+
+    LOG(INFO) << "New job submitted (" << job.description_ << ")";
+  }
+
+
+  ServerScheduler::ServerScheduler(unsigned int maxJobs) : availableJob_(maxJobs)
+  {
+    if (maxJobs == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    finish_ = false;
+    worker_ = boost::thread(Worker, this);
+  }
+
+
+  ServerScheduler::~ServerScheduler()
+  {
+    if (!finish_)
+    {
+      LOG(ERROR) << "INTERNAL ERROR: ServerScheduler::Finalize() should be invoked manually to avoid mess in the destruction order!";
+      Stop();
+    }
+  }
+
+
+  void ServerScheduler::Stop()
+  {
+    if (!finish_)
+    {
+      finish_ = true;
+
+      if (worker_.joinable())
+      {
+        worker_.join();
+      }
+    }
+  }
+
+
+  void ServerScheduler::Submit(ServerJob& job)
+  {
+    if (job.filters_.empty())
+    {
+      return;
+    }
+
+    SubmitInternal(job, false);
+  }
+
+
+  bool ServerScheduler::SubmitAndWait(ListOfStrings& outputs,
+                                      ServerJob& job)
+  {
+    std::string jobId = job.GetId();
+
+    outputs.clear();
+
+    if (job.filters_.empty())
+    {
+      return true;
+    }
+
+    // Add a sink filter to collect all the results of the filters
+    // that have no next filter.
+    ServerCommandInstance& sink = job.AddCommand(new Sink(outputs));
+
+    for (std::list<ServerCommandInstance*>::iterator
+           it = job.filters_.begin(); it != job.filters_.end(); ++it)
+    {
+      if ((*it) != &sink &&
+          (*it)->IsConnectedToSink())
+      {
+        (*it)->ConnectOutput(sink);
+      }
+    }
+
+    // Submit the job
+    SubmitInternal(job, true);
+
+    // Wait for the job to complete (either success or failure)
+    JobStatus status;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      assert(watchedJobStatus_.find(jobId) != watchedJobStatus_.end());
+        
+      while (watchedJobStatus_[jobId] == JobStatus_Running)
+      {
+        watchedJobFinished_.wait(lock);
+      }
+
+      status = watchedJobStatus_[jobId];
+      watchedJobStatus_.erase(jobId);
+    }
+
+    return (status == JobStatus_Success);
+  }
+
+
+  bool ServerScheduler::SubmitAndWait(ServerJob& job)
+  {
+    ListOfStrings ignoredSink;
+    return SubmitAndWait(ignoredSink, job);
+  }
+
+
+  bool ServerScheduler::IsRunning(const std::string& jobId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return jobs_.find(jobId) != jobs_.end();
+  }
+
+
+  void ServerScheduler::Cancel(const std::string& jobId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Jobs::iterator job = jobs_.find(jobId);
+
+    if (job != jobs_.end())
+    {
+      job->second.cancel_ = true;
+      LOG(WARNING) << "Canceling a job (" << job->second.description_ << ")";
+    }
+  }
+
+
+  float ServerScheduler::GetProgress(const std::string& jobId) 
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Jobs::iterator job = jobs_.find(jobId);
+
+    if (job == jobs_.end() || 
+        job->second.size_ == 0  /* should never happen */)
+    {
+      // This job is not running
+      return 1;
+    }
+
+    if (job->second.failures_ != 0)
+    {
+      return 1;
+    }
+
+    if (job->second.size_ == 1)
+    {
+      return static_cast<float>(job->second.success_);
+    }
+
+    return (static_cast<float>(job->second.success_) / 
+            static_cast<float>(job->second.size_ - 1));
+  }
+
+
+  void ServerScheduler::GetListOfJobs(ListOfStrings& jobs)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    jobs.clear();
+
+    for (Jobs::const_iterator 
+           it = jobs_.begin(); it != jobs_.end(); ++it)
+    {
+      jobs.push_back(it->first);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ServerScheduler.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,123 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ServerJob.h"
+
+#include "../../Core/MultiThreading/Semaphore.h"
+
+namespace Orthanc
+{
+  class ServerScheduler : public ServerCommandInstance::IListener
+  {
+  private:
+    struct JobInfo
+    {
+      bool watched_;
+      bool cancel_;
+      size_t size_;
+      size_t success_;
+      size_t failures_;
+      std::string description_;
+    };
+
+    enum JobStatus
+    {
+      JobStatus_Running = 1,
+      JobStatus_Success = 2,
+      JobStatus_Failure = 3
+    };
+
+    typedef IServerCommand::ListOfStrings  ListOfStrings;
+    typedef std::map<std::string, JobInfo> Jobs;
+
+    boost::mutex mutex_;
+    boost::condition_variable watchedJobFinished_;
+    Jobs jobs_;
+    SharedMessageQueue queue_;
+    bool finish_;
+    boost::thread worker_;
+    std::map<std::string, JobStatus> watchedJobStatus_;
+    Semaphore availableJob_;
+
+    JobInfo& GetJobInfo(const std::string& jobId);
+
+    virtual void SignalSuccess(const std::string& jobId);
+
+    virtual void SignalFailure(const std::string& jobId);
+
+    static void Worker(ServerScheduler* that);
+
+    void SubmitInternal(ServerJob& job,
+                        bool watched);
+
+  public:
+    explicit ServerScheduler(unsigned int maxjobs);
+
+    ~ServerScheduler();
+
+    void Stop();
+
+    void Submit(ServerJob& job);
+
+    bool SubmitAndWait(ListOfStrings& outputs,
+                       ServerJob& job);
+
+    bool SubmitAndWait(ServerJob& job);
+
+    bool IsRunning(const std::string& jobId);
+
+    void Cancel(const std::string& jobId);
+
+    // Returns a number between 0 and 1
+    float GetProgress(const std::string& jobId);
+
+    bool IsRunning(const ServerJob& job)
+    {
+      return IsRunning(job.GetId());
+    }
+
+    void Cancel(const ServerJob& job) 
+    {
+      Cancel(job.GetId());
+    }
+
+    float GetProgress(const ServerJob& job) 
+    {
+      return GetProgress(job.GetId());
+    }
+
+    void GetListOfJobs(ListOfStrings& jobs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/StorePeerCommand.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,92 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "StorePeerCommand.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/HttpClient.h"
+
+namespace Orthanc
+{
+  StorePeerCommand::StorePeerCommand(ServerContext& context,
+                                     const WebServiceParameters& peer,
+                                     bool ignoreExceptions) : 
+    context_(context),
+    peer_(peer),
+    ignoreExceptions_(ignoreExceptions)
+  {
+  }
+
+  bool StorePeerCommand::Apply(ListOfStrings& outputs,
+                               const ListOfStrings& inputs)
+  {
+    // Configure the HTTP client
+    HttpClient client(peer_, "instances");
+    client.SetMethod(HttpMethod_Post);
+
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Sending resource " << *it << " to peer \"" 
+                << peer_.GetUrl() << "\"";
+
+      try
+      {
+        context_.ReadDicom(client.GetBody(), *it);
+
+        std::string answer;
+        if (!client.Apply(answer))
+        {
+          LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << peer_.GetUrl() << "\"";
+          throw OrthancException(ErrorCode_NetworkProtocol);
+        }
+
+        // Only chain with other commands if this command succeeds
+        outputs.push_back(*it);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Unable to forward to an Orthanc peer in (instance "
+                   << *it << ", peer " << peer_.GetUrl() << "): " << e.What();
+
+        if (!ignoreExceptions_)
+        {
+          throw;
+        }
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/StorePeerCommand.h	Fri Oct 12 15:18:10 2018 +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
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IServerCommand.h"
+#include "../ServerContext.h"
+#include "../OrthancInitialization.h"
+
+namespace Orthanc
+{
+  class StorePeerCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+    WebServiceParameters peer_;
+    bool ignoreExceptions_;
+
+  public:
+    StorePeerCommand(ServerContext& context,
+                     const WebServiceParameters& peer,
+                     bool ignoreExceptions);
+    
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/StoreScuCommand.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,99 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "StoreScuCommand.h"
+
+#include "../../Core/Logging.h"
+
+namespace Orthanc
+{
+  StoreScuCommand::StoreScuCommand(ServerContext& context,
+                                   const std::string& localAet,
+                                   const RemoteModalityParameters& modality,
+                                   bool ignoreExceptions) : 
+    context_(context),
+    modality_(modality),
+    ignoreExceptions_(ignoreExceptions),
+    localAet_(localAet),
+    moveOriginatorID_(0)
+  {
+  }
+
+
+  void StoreScuCommand::SetMoveOriginator(const std::string& aet,
+                                          uint16_t id)
+  {
+    moveOriginatorAET_ = aet;
+    moveOriginatorID_ = id;
+  }
+
+
+  bool StoreScuCommand::Apply(ListOfStrings& outputs,
+                             const ListOfStrings& inputs)
+  {
+    ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_);
+
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Sending resource " << *it << " to modality \"" 
+                << modality_.GetApplicationEntityTitle() << "\"";
+
+      try
+      {
+        std::string dicom;
+        context_.ReadDicom(dicom, *it);
+
+        locker.GetConnection().Store(dicom, moveOriginatorAET_, moveOriginatorID_);
+
+        // Only chain with other commands if this command succeeds
+        outputs.push_back(*it);
+      }
+      catch (OrthancException& e)
+      {
+        // Ignore transmission errors (e.g. if the remote modality is
+        // powered off)
+        LOG(ERROR) << "Unable to forward to a modality in (instance "
+                   << *it << "): " << e.What();
+
+        if (!ignoreExceptions_)
+        {
+          throw;
+        }
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/StoreScuCommand.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,63 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IServerCommand.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class StoreScuCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+    RemoteModalityParameters modality_;
+    bool ignoreExceptions_;
+    std::string localAet_;
+    std::string moveOriginatorAET_;
+    uint16_t moveOriginatorID_;
+
+  public:
+    StoreScuCommand(ServerContext& context,
+                    const std::string& localAet,
+                    const RemoteModalityParameters& modality,
+                    bool ignoreExceptions);
+
+    void SetMoveOriginator(const std::string& aet,
+                           uint16_t id);
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/SetupAnonymization2011.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,258 @@
+  /**
+   * This is a manual implementation by Alain Mazy. Only kept for reference.
+   * https://bitbucket.org/sjodogne/orthanc/commits/c6defdc4c611fca2ab528ba2c6937a742e0329a8?at=issue-46-anonymization
+   **/
+  
+  void DicomModification::SetupAnonymization2011()
+  {
+    // This is Table E.1-1 from PS 3.15-2011 - DICOM Part 15: Security and System Management Profiles
+    // https://raw.githubusercontent.com/jodogne/dicom-specification/master/2011/11_15pu.pdf
+    
+    removals_.insert(DicomTag(0x0000, 0x1000));  // Affected SOP Instance UID
+    removals_.insert(DicomTag(0x0000, 0x1001));  // Requested SOP Instance UID
+    removals_.insert(DicomTag(0x0002, 0x0003));  // Media Storage SOP Instance UID => TODO: replace with a non-zero length UID that is internally consistent within a set of Instances
+    removals_.insert(DicomTag(0x0004, 0x1511));  // Referenced SOP Instance UID in File
+    removals_.insert(DicomTag(0x0008, 0x0010));  // Irradiation Event UID
+    removals_.insert(DicomTag(0x0008, 0x0014));  // Instance Creator UID
+    //removals_.insert(DicomTag(0x0008, 0x0018));  // SOP Instance UID => set in Apply()
+    clearings_.insert(DicomTag(0x0008, 0x0020)); // Study Date
+    clearings_.insert(DicomTag(0x0008, 0x0021)); // Series Date
+    clearings_.insert(DicomTag(0x0008, 0x0030)); // Study Time
+    clearings_.insert(DicomTag(0x0008, 0x0031)); // Series Time
+    removals_.insert(DicomTag(0x0008, 0x0022));  // Acquisition Date
+    removals_.insert(DicomTag(0x0008, 0x0023));  // Content Date
+    removals_.insert(DicomTag(0x0008, 0x0024));  // Overlay Date
+    removals_.insert(DicomTag(0x0008, 0x0025));  // Curve Date
+    removals_.insert(DicomTag(0x0008, 0x002a));  // Acquisition DateTime
+    removals_.insert(DicomTag(0x0008, 0x0032));  // Acquisition Time
+    removals_.insert(DicomTag(0x0008, 0x0033));  // Content Time
+    removals_.insert(DicomTag(0x0008, 0x0034));  // Overlay Time
+    removals_.insert(DicomTag(0x0008, 0x0035));  // Curve Time
+    removals_.insert(DicomTag(0x0008, 0x0050));  // Accession Number
+    removals_.insert(DicomTag(0x0008, 0x0058));  // Failed SOP Instance UID List
+    removals_.insert(DicomTag(0x0008, 0x0080));  // Institution Name
+    removals_.insert(DicomTag(0x0008, 0x0081));  // Institution Address
+    removals_.insert(DicomTag(0x0008, 0x0082));  // Institution Code Sequence
+    removals_.insert(DicomTag(0x0008, 0x0090));  // Referring Physician's Name
+    removals_.insert(DicomTag(0x0008, 0x0092));  // Referring Physician's Address 
+    removals_.insert(DicomTag(0x0008, 0x0094));  // Referring Physician's Telephone Numbers 
+    removals_.insert(DicomTag(0x0008, 0x0096));  // Referring Physician's Identification Sequence
+    removals_.insert(DicomTag(0x0008, 0x010d));  // Context Group Extension Creator UID
+    removals_.insert(DicomTag(0x0008, 0x0201));  // Timezone Offset From UTC
+    removals_.insert(DicomTag(0x0008, 0x0300));  // Current Patient Location
+    removals_.insert(DicomTag(0x0008, 0x1010));  // Station Name
+    removals_.insert(DicomTag(0x0008, 0x1030));  // Study Description 
+    removals_.insert(DicomTag(0x0008, 0x103e));  // Series Description 
+    removals_.insert(DicomTag(0x0008, 0x1040));  // Institutional Department Name 
+    removals_.insert(DicomTag(0x0008, 0x1048));  // Physician(s) of Record 
+    removals_.insert(DicomTag(0x0008, 0x1049));  // Physician(s) of Record Identification Sequence
+    removals_.insert(DicomTag(0x0008, 0x1050));  // Performing Physicians' Name
+    removals_.insert(DicomTag(0x0008, 0x1052));  // Performing Physicians Identification Sequence
+    removals_.insert(DicomTag(0x0008, 0x1060));  // Name of Physician(s) Reading Study
+    removals_.insert(DicomTag(0x0008, 0x1062));  // Physician Reading Study Identification Sequence
+    removals_.insert(DicomTag(0x0008, 0x1070));  // Operators' Name
+    removals_.insert(DicomTag(0x0008, 0x1072));  // Operators' Identification Sequence
+    removals_.insert(DicomTag(0x0008, 0x1080));  // Admitting Diagnoses Description
+    removals_.insert(DicomTag(0x0008, 0x1084));  // Admitting Diagnoses Code Sequence
+    removals_.insert(DicomTag(0x0008, 0x1110));  // Referenced Study Sequence
+    removals_.insert(DicomTag(0x0008, 0x1111));  // Referenced Performed Procedure Step Sequence
+    removals_.insert(DicomTag(0x0008, 0x1120));  // Referenced Patient Sequence
+    removals_.insert(DicomTag(0x0008, 0x1140));  // Referenced Image Sequence
+    removals_.insert(DicomTag(0x0008, 0x1155));  // Referenced SOP Instance UID
+    removals_.insert(DicomTag(0x0008, 0x1195));  // Transaction UID
+    removals_.insert(DicomTag(0x0008, 0x2111));  // Derivation Description
+    removals_.insert(DicomTag(0x0008, 0x2112));  // Source Image Sequence
+    removals_.insert(DicomTag(0x0008, 0x4000));  // Identifying Comments
+    removals_.insert(DicomTag(0x0008, 0x9123));  // Creator Version UID
+    //removals_.insert(DicomTag(0x0010, 0x0010));  // Patient's Name => cf. below (*)
+    //removals_.insert(DicomTag(0x0010, 0x0020));  // Patient ID => cf. below (*)
+    removals_.insert(DicomTag(0x0010, 0x0030));  // Patient's Birth Date 
+    removals_.insert(DicomTag(0x0010, 0x0032));  // Patient's Birth Time 
+    clearings_.insert(DicomTag(0x0010, 0x0040)); // Patient's Sex
+    removals_.insert(DicomTag(0x0010, 0x0050));  // Patient's Insurance Plan Code Sequence
+    removals_.insert(DicomTag(0x0010, 0x0101));  // Patient's Primary Language Code Sequence
+    removals_.insert(DicomTag(0x0010, 0x0102));  // Patient's Primary Language Modifier Code Sequence
+    removals_.insert(DicomTag(0x0010, 0x1000));  // Other Patient Ids
+    removals_.insert(DicomTag(0x0010, 0x1001));  // Other Patient Names 
+    removals_.insert(DicomTag(0x0010, 0x1002));  // Other Patient IDs Sequence
+    removals_.insert(DicomTag(0x0010, 0x1005));  // Patient's Birth Name
+    removals_.insert(DicomTag(0x0010, 0x1010));  // Patient's Age
+    removals_.insert(DicomTag(0x0010, 0x1020));  // Patient's Size 
+    removals_.insert(DicomTag(0x0010, 0x1030));  // Patient's Weight 
+    removals_.insert(DicomTag(0x0010, 0x1040));  // Patient's Address
+    removals_.insert(DicomTag(0x0010, 0x1050));  // Insurance Plan Identification
+    removals_.insert(DicomTag(0x0010, 0x1060));  // Patient's Mother's Birth Name
+    removals_.insert(DicomTag(0x0010, 0x1080));  // Military Rank
+    removals_.insert(DicomTag(0x0010, 0x1081));  // Branch of Service
+    removals_.insert(DicomTag(0x0010, 0x1090));  // Medical Record Locator
+    removals_.insert(DicomTag(0x0010, 0x2000));  // Medical Alerts
+    removals_.insert(DicomTag(0x0010, 0x2110));  // Allergies
+    removals_.insert(DicomTag(0x0010, 0x2150));  // Country of Residence
+    removals_.insert(DicomTag(0x0010, 0x2152));  // Region of Residence
+    removals_.insert(DicomTag(0x0010, 0x2154));  // PatientTelephoneNumbers
+    removals_.insert(DicomTag(0x0010, 0x2160));  // Ethnic Group
+    removals_.insert(DicomTag(0x0010, 0x2180));  // Occupation 
+    removals_.insert(DicomTag(0x0010, 0x21a0));  // Smoking Status
+    removals_.insert(DicomTag(0x0010, 0x21b0));  // Additional Patient's History
+    removals_.insert(DicomTag(0x0010, 0x21c0));  // Pregnancy Status
+    removals_.insert(DicomTag(0x0010, 0x21d0));  // Last Menstrual Date
+    removals_.insert(DicomTag(0x0010, 0x21f0));  // Patient's Religious Preference
+    removals_.insert(DicomTag(0x0010, 0x2203));  // Patient's Sex Neutered
+    removals_.insert(DicomTag(0x0010, 0x2297));  // Responsible Person
+    removals_.insert(DicomTag(0x0010, 0x2299));  // Responsible Organization
+    removals_.insert(DicomTag(0x0010, 0x4000));  // Patient Comments
+    removals_.insert(DicomTag(0x0018, 0x0010));  // Contrast Bolus Agent
+    removals_.insert(DicomTag(0x0018, 0x1000));  // Device Serial Number
+    removals_.insert(DicomTag(0x0018, 0x1002));  // Device UID
+    removals_.insert(DicomTag(0x0018, 0x1004));  // Plate ID
+    removals_.insert(DicomTag(0x0018, 0x1005));  // Generator ID
+    removals_.insert(DicomTag(0x0018, 0x1007));  // Cassette ID
+    removals_.insert(DicomTag(0x0018, 0x1008));  // Gantry ID
+    removals_.insert(DicomTag(0x0018, 0x1030));  // Protocol Name
+    removals_.insert(DicomTag(0x0018, 0x1400));  // Acquisition Device Processing Description
+    removals_.insert(DicomTag(0x0018, 0x4000));  // Acquisition Comments
+    removals_.insert(DicomTag(0x0018, 0x700a));  // Detector ID
+    removals_.insert(DicomTag(0x0018, 0xa003));  // Contribution Description
+    removals_.insert(DicomTag(0x0018, 0x9424));  // Acquisition Protocol Description
+    //removals_.insert(DicomTag(0x0020, 0x000d));  // Study Instance UID => set in Apply()
+    //removals_.insert(DicomTag(0x0020, 0x000e));  // Series Instance UID => set in Apply()
+    removals_.insert(DicomTag(0x0020, 0x0010));  // Study ID
+    removals_.insert(DicomTag(0x0020, 0x0052));  // Frame of Reference UID 
+    removals_.insert(DicomTag(0x0020, 0x0200));  // Synchronization Frame of Reference UID 
+    removals_.insert(DicomTag(0x0020, 0x3401));  // Modifying Device ID
+    removals_.insert(DicomTag(0x0020, 0x3404));  // Modifying Device Manufacturer
+    removals_.insert(DicomTag(0x0020, 0x3406));  // Modified Image Description
+    removals_.insert(DicomTag(0x0020, 0x4000));  // Image Comments
+    removals_.insert(DicomTag(0x0020, 0x9158));  // Frame Comments
+    removals_.insert(DicomTag(0x0020, 0x9161));  // Concatenation UID
+    removals_.insert(DicomTag(0x0020, 0x9164));  // Dimension Organization UID
+    //removals_.insert(DicomTag(0x0028, 0x1199));  // Palette Color Lookup Table UID => TODO: replace with a non-zero length UID that is internally consistent within a set of Instances
+    //removals_.insert(DicomTag(0x0028, 0x1214));  // Large Palette Color Lookup Table UID => TODO: replace with a non-zero length UID that is internally consistent within a set of Instances
+    removals_.insert(DicomTag(0x0028, 0x4000));  // Image Presentation Comments
+    removals_.insert(DicomTag(0x0032, 0x0012));  // Study ID Issuer
+    removals_.insert(DicomTag(0x0032, 0x1020));  // Scheduled Study Location
+    removals_.insert(DicomTag(0x0032, 0x1021));  // Scheduled Study Location AE Title
+    removals_.insert(DicomTag(0x0032, 0x1030));  // Reason for Study
+    removals_.insert(DicomTag(0x0032, 0x1032));  // Requesting Physician
+    removals_.insert(DicomTag(0x0032, 0x1033));  // Requesting Service
+    removals_.insert(DicomTag(0x0032, 0x1060));  // Requesting Procedure Description
+    removals_.insert(DicomTag(0x0032, 0x1070));  // Requested Contrast Agent
+    removals_.insert(DicomTag(0x0032, 0x4000));  // Study Comments
+    removals_.insert(DicomTag(0x0038, 0x0010));  // Admission ID
+    removals_.insert(DicomTag(0x0038, 0x0011));  // Issuer of Admission ID
+    removals_.insert(DicomTag(0x0038, 0x001e));  // Scheduled Patient Institution Residence
+    removals_.insert(DicomTag(0x0038, 0x0020));  // Admitting Date
+    removals_.insert(DicomTag(0x0038, 0x0021));  // Admitting Time
+    removals_.insert(DicomTag(0x0038, 0x0040));  // Discharge Diagnosis Description
+    removals_.insert(DicomTag(0x0038, 0x0050));  // Special Needs
+    removals_.insert(DicomTag(0x0038, 0x0060));  // Service Episode ID
+    removals_.insert(DicomTag(0x0038, 0x0061));  // Issuer of Service Episode ID
+    removals_.insert(DicomTag(0x0038, 0x0062));  // Service Episode Description
+    removals_.insert(DicomTag(0x0038, 0x0400));  // Patient's Institution Residence
+    removals_.insert(DicomTag(0x0038, 0x0500));  // Patient State
+    removals_.insert(DicomTag(0x0038, 0x4000));  // Visit Comments
+    removals_.insert(DicomTag(0x0038, 0x1234));  // Referenced Patient Alias Sequence
+    removals_.insert(DicomTag(0x0040, 0x0001));  // Scheduled Station AE Title
+    removals_.insert(DicomTag(0x0040, 0x0002));  // Scheduled Procedure Step Start Date
+    removals_.insert(DicomTag(0x0040, 0x0003));  // Scheduled Procedure Step Start Time
+    removals_.insert(DicomTag(0x0040, 0x0004));  // Scheduled Procedure Step End Date
+    removals_.insert(DicomTag(0x0040, 0x0005));  // Scheduled Procedure Step End Time
+    removals_.insert(DicomTag(0x0040, 0x0006));  // Scheduled Performing Physician Name
+    removals_.insert(DicomTag(0x0040, 0x0007));  // Scheduled Procedure Step Description
+    removals_.insert(DicomTag(0x0040, 0x000b));  // Scheduled Performing Physician Identification Sequence
+    removals_.insert(DicomTag(0x0040, 0x0010));  // Scheduled Station Name
+    removals_.insert(DicomTag(0x0040, 0x0011));  // Scheduled Procedure Step Location
+    removals_.insert(DicomTag(0x0040, 0x0012));  // Pre-Medication
+    removals_.insert(DicomTag(0x0040, 0x0241));  // Performed Station AE Title
+    removals_.insert(DicomTag(0x0040, 0x0242));  // Performed Station Name
+    removals_.insert(DicomTag(0x0040, 0x0243));  // Performed Location
+    removals_.insert(DicomTag(0x0040, 0x0244));  // Performed Procedure Step Start Date
+    removals_.insert(DicomTag(0x0040, 0x0245));  // Performed Procedure Step Start Time
+    removals_.insert(DicomTag(0x0040, 0x0248));  // Performed Station Name Code Sequence
+    removals_.insert(DicomTag(0x0040, 0x0253));  // Performed Procedure Step ID
+    removals_.insert(DicomTag(0x0040, 0x0254));  // Performed Procedure Step Description
+    removals_.insert(DicomTag(0x0040, 0x0275));  // Request Attributes Sequence
+    removals_.insert(DicomTag(0x0040, 0x0280));  // Comments on Performed Procedure Step
+    removals_.insert(DicomTag(0x0040, 0x0555));  // Acquisition Context Sequence
+    removals_.insert(DicomTag(0x0040, 0x1001));  // Requested Procedure ID
+    removals_.insert(DicomTag(0x0040, 0x1010));  // Names of Intended Recipient of Results
+    removals_.insert(DicomTag(0x0040, 0x1011));  // Intended Recipient of Results Identification Sequence
+    removals_.insert(DicomTag(0x0040, 0x1004));  // Patient Transport Arrangements
+    removals_.insert(DicomTag(0x0040, 0x1005));  // Requested Procedure Location
+    removals_.insert(DicomTag(0x0040, 0x1101));  // Person Identification Code Sequence
+    removals_.insert(DicomTag(0x0040, 0x1102));  // Person Address
+    removals_.insert(DicomTag(0x0040, 0x1103));  // Person Telephone Numbers
+    removals_.insert(DicomTag(0x0040, 0x1400));  // Requested Procedure Comments
+    removals_.insert(DicomTag(0x0040, 0x2001));  // Reason for Imaging Service Request
+    removals_.insert(DicomTag(0x0040, 0x2008));  // Order Entered By
+    removals_.insert(DicomTag(0x0040, 0x2009));  // Order Enterer Location
+    removals_.insert(DicomTag(0x0040, 0x2010));  // Order Callback Phone Number
+    removals_.insert(DicomTag(0x0040, 0x2016));  // Placer Order Number of Imaging Service Request
+    removals_.insert(DicomTag(0x0040, 0x2017));  // Filler Order Number of Imaging Service Request
+    removals_.insert(DicomTag(0x0040, 0x2400));  // Imaging Service Request Comments
+    removals_.insert(DicomTag(0x0040, 0x4023));  // Referenced General Purpose Scheduled Procedure Step Transaction UID
+    removals_.insert(DicomTag(0x0040, 0x4025));  // Scheduled Station Name Code Sequence
+    removals_.insert(DicomTag(0x0040, 0x4027));  // Scheduled Station Geographic Location Code Sequence
+    removals_.insert(DicomTag(0x0040, 0x4030));  // Performed Station Geographic Location Code Sequence
+    removals_.insert(DicomTag(0x0040, 0x4034));  // Scheduled Human Performers Sequence
+    removals_.insert(DicomTag(0x0040, 0x4035));  // Actual Human Performers Sequence
+    removals_.insert(DicomTag(0x0040, 0x4036));  // Human Performers Organization
+    removals_.insert(DicomTag(0x0040, 0x4037));  // Human Performers Name
+    removals_.insert(DicomTag(0x0040, 0xa027));  // Verifying Organization
+    removals_.insert(DicomTag(0x0040, 0xa073));  // Verifying Observer Sequence
+    removals_.insert(DicomTag(0x0040, 0xa075));  // Verifying Observer Name
+    removals_.insert(DicomTag(0x0040, 0xa078));  // Author Observer Sequence
+    removals_.insert(DicomTag(0x0040, 0xa07a));  // Participant Sequence
+    removals_.insert(DicomTag(0x0040, 0xa07c));  // Custodial Organization Sequence
+    removals_.insert(DicomTag(0x0040, 0xa088));  // Verifying Observer Identification Code Sequence
+    removals_.insert(DicomTag(0x0040, 0xa123));  // Person Name
+    removals_.insert(DicomTag(0x0040, 0xa124));  // UID
+    removals_.insert(DicomTag(0x0040, 0xa730));  // Content Sequence 
+    removals_.insert(DicomTag(0x0040, 0x3001));  // Confidentiality Constraint on Patient Data Description
+    removals_.insert(DicomTag(0x0040, 0xdb0c));  // Template Extension Organization UID
+    removals_.insert(DicomTag(0x0040, 0xdb0d));  // Template Extension Creator UID
+    removals_.insert(DicomTag(0x0070, 0x0001));  // Graphic Annotation Sequence
+    removals_.insert(DicomTag(0x0070, 0x0084));  // Content Creator's Name
+    removals_.insert(DicomTag(0x0070, 0x0086));  // Content Creator's Identification Code Sequence
+    removals_.insert(DicomTag(0x0070, 0x031a));  // Fiducial UID
+    removals_.insert(DicomTag(0x0088, 0x0140));  // Storage Media File-set UID
+    removals_.insert(DicomTag(0x0088, 0x0200));  // Icon Image Sequence
+    removals_.insert(DicomTag(0x0088, 0x0904));  // Topic Title
+    removals_.insert(DicomTag(0x0088, 0x0906));  // Topic Subject
+    removals_.insert(DicomTag(0x0088, 0x0910));  // Topic Author
+    removals_.insert(DicomTag(0x0088, 0x0912));  // Topic Key Words
+    removals_.insert(DicomTag(0x0400, 0x0100));  // Digital Signature UID
+    removals_.insert(DicomTag(0x0400, 0x0402));  // Referenced Digital Signature Sequence
+    removals_.insert(DicomTag(0x0400, 0x0403));  // Referenced SOP Instance MAC Sequence
+    removals_.insert(DicomTag(0x0400, 0x0404));  // MAC
+    removals_.insert(DicomTag(0x0400, 0x0550));  // Modified Attributes Sequence
+    removals_.insert(DicomTag(0x0400, 0x0561));  // Original Attributes Sequence
+    removals_.insert(DicomTag(0x2030, 0x0020));  // Text String
+    removals_.insert(DicomTag(0x3006, 0x0024));  // Referenced Frame of Reference UID
+    removals_.insert(DicomTag(0x3006, 0x00c2));  // Related Frame of Reference UID 
+    removals_.insert(DicomTag(0x300a, 0x0013));  // Dose Reference UID
+    removals_.insert(DicomTag(0x300e, 0x0008));  // Reviewer Name
+    removals_.insert(DicomTag(0x4000, 0x0010));  // Arbitrary
+    removals_.insert(DicomTag(0x4000, 0x4000));  // Text Comments
+    removals_.insert(DicomTag(0x4008, 0x0042));  // Results ID Issuer
+    removals_.insert(DicomTag(0x4008, 0x0102));  // Interpretation Recorder
+    removals_.insert(DicomTag(0x4008, 0x010a));  // Interpretation Transcriber
+    removals_.insert(DicomTag(0x4008, 0x010b));  // Interpretation Text
+    removals_.insert(DicomTag(0x4008, 0x010c));  // Interpretation Author
+    removals_.insert(DicomTag(0x4008, 0x0111));  // Interpretation Approver Sequence
+    removals_.insert(DicomTag(0x4008, 0x0114));  // Physician Approving Interpretation
+    removals_.insert(DicomTag(0x4008, 0x0115));  // Interpretation Diagnosis Description
+    removals_.insert(DicomTag(0x4008, 0x0118));  // Results Distribution List Sequence
+    removals_.insert(DicomTag(0x4008, 0x0119));  // Distribution Name
+    removals_.insert(DicomTag(0x4008, 0x011a));  // Distribution Address
+    removals_.insert(DicomTag(0x4008, 0x0202));  // Interpretation ID Issuer
+    removals_.insert(DicomTag(0x4008, 0x0300));  // Impressions
+    removals_.insert(DicomTag(0x4008, 0x4000));  // Results Comments
+    removals_.insert(DicomTag(0xfffa, 0xfffa));  // Digital Signature Sequence
+    removals_.insert(DicomTag(0xfffc, 0xfffc));  // Data Set Trailing Padding
+    //removals_.insert(DicomTag(0x60xx, 0x4000));  // Overlay Comments => TODO
+    //removals_.insert(DicomTag(0x60xx, 0x3000));  // Overlay Data => TODO
+
+    // Set the DeidentificationMethod tag
+    ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2011);
+  }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/ImplementationNotes/JobsEngineStates.dot	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,28 @@
+// dot -Tpdf JobsEngineStates.dot -o JobsEngineStates.pdf
+
+digraph G
+{
+  rankdir="LR";
+  init [shape=point];
+  failure, success [shape=doublecircle];
+
+  // Internal transitions
+  init -> pending;
+  pending -> running;
+  running -> success;
+  running -> failure;
+  running -> retry;
+  retry -> pending [label="timeout"];
+
+  // External actions
+  failure -> pending  [label="Resubmit()" fontcolor="red"];
+  paused -> pending  [label="Resume()" fontcolor="red"];
+  pending -> paused  [label="Pause()" fontcolor="red"];
+  retry -> paused  [label="Pause()" fontcolor="red"];
+  running -> paused  [label="Pause()" fontcolor="red"];
+
+  paused -> failure  [label="Cancel()" fontcolor="red"];
+  pending -> failure  [label="Cancel()" fontcolor="red"];
+  retry -> failure  [label="Cancel()" fontcolor="red"];
+  running -> failure  [label="Cancel()" fontcolor="red"];
+}
Binary file Resources/ImplementationNotes/JobsEngineStates.pdf has changed
--- a/Resources/LinuxStandardBaseToolchain.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/LinuxStandardBaseToolchain.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -1,14 +1,16 @@
+# LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../Resources/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON
+
 INCLUDE(CMakeForceCompiler)
 
 SET(LSB_PATH $ENV{LSB_PATH})
+SET(LSB_CC $ENV{LSB_CC})
+SET(LSB_CXX $ENV{LSB_CXX})
 SET(LSB_TARGET_VERSION "4.0")
 
 IF ("${LSB_PATH}" STREQUAL "")
   SET(LSB_PATH "/opt/lsb")
 ENDIF()
 
-message("Using the following Linux Standard Base: ${LSB_PATH}")
-
 IF (EXISTS ${LSB_PATH}/lib64)
   SET(LSB_TARGET_PROCESSOR "x86_64")
   SET(LSB_LIBPATH ${LSB_PATH}/lib64-${LSB_TARGET_VERSION})
@@ -38,5 +40,27 @@
 # search headers and libraries in the target environment, search 
 # programs in the host environment
 SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
-SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
-SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER)
+SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
+
+SET(CMAKE_CROSSCOMPILING OFF)
+
+
+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -I${LSB_PATH}/include" CACHE INTERNAL "" FORCE)
+SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -nostdinc++ -I${LSB_PATH}/include -I${LSB_PATH}/include/c++ -I${LSB_PATH}/include/c++/backward" CACHE INTERNAL "" FORCE)
+SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH} --lsb-besteffort" CACHE INTERNAL "" FORCE)
+SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH} --lsb-besteffort" CACHE INTERNAL "" FORCE)
+
+if (NOT "${LSB_CXX}" STREQUAL "")
+  SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-cxx=${LSB_CXX}")
+  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-cxx=${LSB_CXX}")
+  SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-cxx=${LSB_CXX}")
+endif()
+
+if (NOT "${LSB_CC}" STREQUAL "")
+  SET(CMAKE_C_FLAGS "${CMAKE_CC_FLAGS} --lsb-cc=${LSB_CC}")
+  SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-cc=${LSB_CC}")
+  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-cc=${LSB_CC}")
+  SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-cc=${LSB_CC}")
+endif()
+
--- a/Resources/MinGWToolchain.cmake	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/MinGWToolchain.cmake	Fri Oct 12 15:18:10 2018 +0200
@@ -15,3 +15,6 @@
 set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
 set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
 set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DSTACK_SIZE_PARAM_IS_A_RESERVATION=0x10000" CACHE INTERNAL "" FORCE)
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DSTACK_SIZE_PARAM_IS_A_RESERVATION=0x10000" CACHE INTERNAL "" FORCE)
--- a/Resources/OldBuildInstructions.txt	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/OldBuildInstructions.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -32,7 +32,7 @@
 # cmake -DALLOW_DOWNLOADS=ON \
         -DUSE_SYSTEM_GOOGLE_LOG=OFF \
 	-DUSE_SYSTEM_MONGOOSE=OFF \
-        -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
+        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
 	-DUSE_SYSTEM_PUGIXML=OFF \
         -DENABLE_JPEG=OFF \
         -DENABLE_JPEG_LOSSLESS=OFF \
@@ -55,7 +55,7 @@
         -DALLOW_DOWNLOADS=ON \
 	-DUSE_SYSTEM_MONGOOSE=OFF \
 	-DUSE_SYSTEM_JSONCPP=OFF \
-        -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
+        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
 	-DUSE_SYSTEM_PUGIXML=OFF \
 	~/Orthanc
 
@@ -66,7 +66,7 @@
         -DALLOW_DOWNLOADS=ON \
 	-DUSE_SYSTEM_MONGOOSE=OFF \
 	-DUSE_SYSTEM_JSONCPP=OFF \
-        -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
+        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
 	-DUSE_SYSTEM_PUGIXML=OFF \
         -DENABLE_JPEG=OFF \
         -DENABLE_JPEG_LOSSLESS=OFF \
@@ -85,7 +85,7 @@
 # cmake "-DDCMTK_LIBRARIES=wrap;oflog" \
         -DALLOW_DOWNLOADS=ON \
 	-DUSE_SYSTEM_MONGOOSE=OFF \
-        -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
+        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
 	-DUSE_SYSTEM_PUGIXML=OFF \
         -DENABLE_JPEG=OFF \
         -DENABLE_JPEG_LOSSLESS=OFF \
--- a/Resources/Orthanc.doxygen	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Orthanc.doxygen	Fri Oct 12 15:18:10 2018 +0200
@@ -1,110 +1,129 @@
-# Doxyfile 1.8.1.2
+# Doxyfile 1.8.7
 
 # 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.
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single 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 (" ").
+# 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.
+# 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.
+# The default value is: UTF-8.
 
 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.
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
 
 PROJECT_NAME           = Orthanc
 
-# 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.
+# 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.
+# 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          = "Internal documentation of Orthanc"
 
-# 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.
+# 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           = @CMAKE_SOURCE_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.
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. 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       = OrthancInternalDocumentation
 
-# 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.
+# 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 causes
+# performance problems for the file system.
+# The default value is: NO.
 
 CREATE_SUBDIRS         = NO
 
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES    = 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.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
 
 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.
+# If the BRIEF_MEMBER_DESC tag is set to YES 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.
+# The default value is: YES.
 
 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
+# If the REPEAT_BRIEF tag is set to YES 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.
+# The default value is: YES.
 
 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"
+# 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 and 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
+# doxygen will generate a detailed section even if there is only a brief
 # description.
+# The default value is: NO.
 
 ALWAYS_DETAILED_SEC    = NO
 
@@ -112,169 +131,207 @@
 # 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.
+# The default value is: NO.
 
 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.
+# If the FULL_PATH_NAMES tag is set to YES 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
+# The default value is: YES.
 
 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.
+# 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.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
 
 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.
+# 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 list of 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.
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
 
 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.)
+# 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-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
 
 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.)
+# 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 Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
 
 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.
+# 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 behavior. 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 behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
 
 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.
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
 
 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.
+# 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.
+# The default value is: NO.
 
 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.
+# 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.
+# Minimum value: 1, maximum value: 16, default value: 4.
 
 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.
+# This tag can be used to specify a number of aliases that act 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.
+# 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.
+# 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.
+# The default value is: NO.
 
 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.
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
 
 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.
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
 
 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.
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
 
 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.
+# 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,
+# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran:
+# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran:
+# Fortran. In the later case the parser tries to guess whether the code is fixed
+# or free formatted code, this is the default for Fortran type files), VHDL. 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 For files without extension you can use no_extension as a placeholder.
+#
+# 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
+# If the MARKDOWN_SUPPORT tag is enabled 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.
+# 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.
+# The default value is: YES.
 
 MARKDOWN_SUPPORT       = YES
 
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by by putting a % sign in front of the word
+# or globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_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
+# 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);
+# versus func(std::string) {}). This also make the inheritance and collaboration
 # diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
 
 BUILTIN_STL_SUPPORT    = YES
 
 # If you use Microsoft's C++/CLI language, you should set this option to YES to
 # enable parsing support.
+# The default value is: NO.
 
 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.
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# http://www.riverbankcomputing.co.uk/software/sip/intro) 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.
+# The default value is: NO.
 
 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.
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to 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.
+# The default value is: YES.
 
 IDL_PROPERTY_SUPPORT   = YES
 
@@ -282,67 +339,61 @@
 # 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.
+# The default value is: NO.
 
 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.
+# Set the SUBGROUPING tag to YES 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.
+# The default value is: YES.
 
 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).
+# 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).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
 
 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).
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef 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, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
 
 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
+# When TYPEDEF_HIDES_STRUCT tag 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
+# 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.
+# The default value is: NO.
 
 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.
+# 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 appears 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. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
 
 LOOKUP_CACHE_SIZE      = 0
 
@@ -351,340 +402,391 @@
 #---------------------------------------------------------------------------
 
 # 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
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
 
 EXTRACT_ALL            = YES
 
-# If the EXTRACT_PRIVATE tag is set to YES all private members of a class
-# will be included in the documentation.
+# If the EXTRACT_PRIVATE tag is set to YES all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
 
 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.
+# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
 
 EXTRACT_PACKAGE        = NO
 
-# If the EXTRACT_STATIC tag is set to YES all static members of a file
-# will be included in the documentation.
+# If the EXTRACT_STATIC tag is set to YES all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
 
 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.
+# 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. Does not have any effect
+# for Java sources.
+# The default value is: YES.
 
 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.
+# 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 only methods in the interface are
+# included.
+# The default value is: NO.
 
 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.
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
 
 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.
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO 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.
+# The default value is: NO.
 
 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.
+# 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 these classes will be included in the various overviews. This option has
+# no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
 
 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.
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# (class|struct|union) declarations. If set to NO these declarations will be
+# included in the documentation.
+# The default value is: NO.
 
 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.
+# 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 these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
 
 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.
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
 
 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
+# 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.
+# The default value is: system dependent.
 
 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.
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES the
+# scope will be hidden.
+# The default value is: NO.
 
 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.
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
 
 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.
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC  = NO
+
+# 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.
+# The default value is: NO.
 
 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.
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
 
 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.
+# If the SORT_MEMBER_DOCS tag is set to YES 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.
+# The default value is: YES.
 
 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.
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
 
 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.
+# 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 constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: 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.
+# 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 group names will
+# appear in their defined order.
+# The default value is: NO.
 
 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.
+# 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 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.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
 
 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.
+# 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.
+# The default value is: NO.
 
 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.
+# 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.
+# The default value is: YES.
 
 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.
+# 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.
+# The default value is: YES.
 
 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.
+# 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.
+# The default value is: YES.
 
 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.
+# 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.
+# The default value is: YES.
 
 GENERATE_DEPRECATEDLIST= YES
 
-# The ENABLED_SECTIONS tag can be used to enable conditional
-# documentation sections, marked by \if sectionname ... \endif.
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
 
 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.
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have 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 value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
 
 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.
+# 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.
+# The default value is: YES.
 
 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.
+# 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 value 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.
+# 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 value 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.
+# 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. For an example see the documentation.
 
 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.
+# 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.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
 
 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.
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This 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. Do not use file names with spaces, bibtex cannot handle them. See
+# also \cite for info how to create references.
 
 CITE_BIB_FILES         =
 
 #---------------------------------------------------------------------------
-# configuration options related to warning and progress messages
+# 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.
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
 
 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.
+# generated to standard error ( stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
 
 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.
+# If the WARN_IF_UNDOCUMENTED tag 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.
+# The default value is: YES.
 
 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.
+# If the WARN_IF_DOC_ERROR tag 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.
+# The default value is: YES.
 
 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.
+# This 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 doxygen will only warn about wrong or incomplete parameter
+# documentation, but not about the absence of documentation.
+# The default value is: NO.
 
 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)
+# 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)
+# The default value is: $file:$line: $text.
 
 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.
+# 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 standard
+# error (stderr).
 
 WARN_LOGFILE           =
 
 #---------------------------------------------------------------------------
-# configuration options related to the input files
+# 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.
+# The INPUT tag is 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.
+# Note: If this tag is empty the current directory is searched.
 
 INPUT                  = @CMAKE_SOURCE_DIR@/Core \
                          @CMAKE_SOURCE_DIR@/OrthancServer
 
 # 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.
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see: http://www.gnu.org/software/libiconv) for the list of
+# possible encodings.
+# The default value is: UTF-8.
 
 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 tag to specify one or more wildcard patterns (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++, *.java, *.ii,
+# *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp,
+# *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown,
+# *.md, *.mm, *.dox, *.py, *.f90, *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf,
+# *.qsf, *.as and *.js.
 
 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.
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
 
 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.
 
@@ -693,14 +795,16 @@
 # 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.
+# The default value is: NO.
 
 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/*
+# 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       =
 
@@ -709,755 +813,1076 @@
 # 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
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
 
 EXCLUDE_SYMBOLS        = Orthanc::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).
+# 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 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.
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
 
 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).
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be 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.
+# 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.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
 
 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.
+# 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 information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none 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).
+# INPUT_FILTER ) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
 
 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.
+# 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 tag requires that the tag FILTER_SOURCE_FILES is set to YES.
 
 FILTER_SOURCE_PATTERNS =
 
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE =
+
 #---------------------------------------------------------------------------
-# configuration options related to source browsing
+# 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.
+# 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 that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
 
 SOURCE_BROWSER         = NO
 
-# Setting the INLINE_SOURCES tag to YES will include the body
-# of functions and classes directly in the documentation.
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
 
 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.
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
 
 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.
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# function all documented functions referencing it will be listed.
+# The default value is: NO.
 
 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.
+# 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.
+# The default value is: NO.
 
 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.
+# If the REFERENCES_LINK_SOURCE tag is set to YES 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.
+# The default value is: YES.
 
 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.
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS        = 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.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the config file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
 
 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.
+# If the VERBATIM_HEADERS tag is set the YES 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.
+# See also: Section \class.
+# The default value is: YES.
 
 VERBATIM_HEADERS       = YES
 
 #---------------------------------------------------------------------------
-# configuration options related to the alphabetical class index
+# 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.
+# 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.
+# The default value is: YES.
 
 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])
+# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in
+# which the alphabetical index list will be split.
+# Minimum value: 1, maximum value: 20, default value: 5.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
 
 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.
+# 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 a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
 
 IGNORE_PREFIX          =
 
 #---------------------------------------------------------------------------
-# configuration options related to the HTML output
+# Configuration options related to the HTML output
 #---------------------------------------------------------------------------
 
-# If the GENERATE_HTML tag is set to YES (the default) Doxygen will
-# generate HTML output.
+# If the GENERATE_HTML tag is set to YES doxygen will generate HTML output
+# The default value is: YES.
 
 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.
+# 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.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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!
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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!
+# 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 left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 HTML_STYLESHEET        =
 
+# The HTML_EXTRA_STYLESHEET tag can be used to specify an additional user-
+# defined cascading style sheet that is included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefor more robust against future updates.
+# Doxygen will copy the style sheet file to the output directory. For an example
+# see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_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.
+# $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.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the stylesheet 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.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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
+# 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 (see: http://developer.apple.com/tools/xcode/), 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# This tag determines the name of the docset 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.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
 
 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.
+# 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.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
 
 DOCSET_BUNDLE_ID       = org.doxygen.Project
 
-# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely identify
+# The DOCSET_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.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
 
 DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
 
-# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher.
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
 
 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.
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on
+# Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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
+# 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.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
 
 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.
+# 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.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
 
 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).
+# 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).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
 
 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.
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index ( hhk), content ( hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
 
 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.
+# 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. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
 
 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.
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual-
+# folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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>.
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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>.
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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.
+# 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.
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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.
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
 
 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.
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set 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.
+# 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. Via custom stylesheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values 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.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. 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.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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 directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT         = HTML-CSS
+
+# 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.
+# The default value is: http://cdn.mathjax.org/mathjax/latest.
+# This tag requires that the tag USE_MATHJAX is set to YES.
 
 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.
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
 
 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.
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE       =
+
+# 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. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# implemented using a web server instead of a web client using Javascript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
 
 SERVER_BASED_SEARCH    = NO
 
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer ( doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH        = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer ( doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/). See the section "External Indexing and
+# Searching" for details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL       =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE        = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID     =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS  =
+
 #---------------------------------------------------------------------------
-# configuration options related to the LaTeX output
+# Configuration options related to the LaTeX output
 #---------------------------------------------------------------------------
 
-# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will
-# generate Latex output.
+# If the GENERATE_LATEX tag is set to YES doxygen will generate LaTeX output.
+# The default value is: YES.
 
 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.
+# 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.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# invoked.
+#
+# 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.
+# The default file is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. To get the times font for
+# instance you can specify
+# EXTRA_PACKAGES=times
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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!
+# 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. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber. Doxygen will
+# replace them by respectively the title of the page, the current date and time,
+# only the current date, the version number of doxygen, the project name (see
+# PROJECT_NAME), or the project number (see PROJECT_NUMBER).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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!
+# 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.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES      =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). 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.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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
+# If the LATEX_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
+# the PDF file directly from the LaTeX files. Set this option to YES to get a
 # higher quality PDF documentation.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# If the LATEX_SOURCE_CODE tag 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# bibliography, e.g. plainnat, or ieeetr. See
+# http://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 LATEX_BIB_STYLE        = plain
 
 #---------------------------------------------------------------------------
-# configuration options related to the RTF output
+# 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.
+# 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 too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
 
 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.
+# 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.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
 
 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.
+# 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 some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
 
 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.
+# Load stylesheet 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.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
 
 RTF_STYLESHEET_FILE    =
 
-# Set optional variables used in the generation of an rtf document.
-# Syntax is similar to doxygen's config file.
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's config file. A template extensions file can be generated
+# using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
 
 RTF_EXTENSIONS_FILE    =
 
 #---------------------------------------------------------------------------
-# configuration options related to the man page output
+# Configuration options related to the man page output
 #---------------------------------------------------------------------------
 
-# If the GENERATE_MAN tag is set to YES (the default) Doxygen will
-# generate man pages
+# If the GENERATE_MAN tag is set to YES doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
 
 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.
+# 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. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
 
 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)
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
 
 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.
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR             =
+
+# 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 value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
 
 MAN_LINKS              = NO
 
 #---------------------------------------------------------------------------
-# configuration options related to the XML output
+# 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.
+# 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.
+# The default value is: NO.
 
 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.
+# 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.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
 
 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.
+# 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.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
 
 XML_PROGRAMLISTING     = YES
 
 #---------------------------------------------------------------------------
-# configuration options for the AutoGen Definitions output
+# Configuration options related to the DOCBOOK 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.
+# If the GENERATE_DOCBOOK tag is set to YES doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK       = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT         = docbook
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES doxygen will generate an AutoGen
+# Definitions (see http://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.
+# The default value is: NO.
 
 GENERATE_AUTOGEN_DEF   = NO
 
 #---------------------------------------------------------------------------
-# configuration options related to the Perl module output
+# 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.
+# 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.
+# The default value is: NO.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
 
 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.
+# 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.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
 
 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.
+# 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.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
 
 PERLMOD_MAKEVAR_PREFIX =
 
@@ -1465,106 +1890,128 @@
 # 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.
+# If the ENABLE_PREPROCESSING tag is set to YES doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
 
 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.
+# If the MACRO_EXPANSION tag is set to YES doxygen will expand all macro names
+# in the source code. If set to NO only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
 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.
+# If the SEARCH_INCLUDES tag is set to YES the includes files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
 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.
+# contain include files that are not input files but should be processed by the
+# preprocessor.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
 
 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.
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
 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.
+# 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 e.g.
+# 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.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
 PREDEFINED             =
 
-# 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.
+# 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.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
 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.
+# If the SKIP_FUNCTION_MACROS tag is set to YES 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. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
 SKIP_FUNCTION_MACROS   = YES
 
 #---------------------------------------------------------------------------
-# Configuration::additions related to external references
+# Configuration options 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:
-#
+# The TAGFILES tag can be used to specify one or more tag files. 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.
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: 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.
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
 
 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.
+# If the ALLEXTERNALS tag is set to YES all external class will be listed in the
+# class index. If set to NO only the inherited external classes will be listed.
+# The default value is: NO.
 
 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.
+# 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.
+# The default value is: YES.
 
 EXTERNAL_GROUPS        = YES
 
+# If the EXTERNAL_PAGES tag is set to YES all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES         = YES
+
 # The PERL_PATH should be the absolute path and name of the perl script
-# interpreter (i.e. the result of `which perl').
+# interpreter (i.e. the result of 'which perl').
+# The default file (with absolute path) is: /usr/bin/perl.
 
 PERL_PATH              = /usr/bin/perl
 
@@ -1572,222 +2019,295 @@
 # 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.
+# If the CLASS_DIAGRAMS tag is set to YES doxygen will generate a class diagram
+# (in HTML 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.
+# The default value is: YES.
 
 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
+# 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.
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_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.
+# The default value is: YES.
 
 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)
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), 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 value is: YES.
 
 HAVE_DOT               = NO
 
-# 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.
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 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.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# When you want a differently looking font n the dot files that doxygen
+# generates 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.
+# The default value is: Helvetica.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
+# dot graphs.
+# Minimum value: 4, maximum value: 24, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# By default doxygen will tell dot to use the default font as specified with
+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
+# the path where dot can find it using this tag.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the CLASS_GRAPH tag is 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.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the COLLABORATION_GRAPH tag is 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.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 COLLABORATION_GRAPH    = YES
 
-# 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
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# 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 manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 UML_LIMIT_NUM_FIELDS   = 10
 
-# If set to YES, the inheritance and collaboration graphs will show the
-# relations between templates and their instances.
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES 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.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES 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.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the CALL_GRAPH tag is 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.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the CALLER_GRAPH tag is 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.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the DIRECTORY_GRAPH tag is 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.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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).
+# generated by dot.
+# Note: 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).
+# Possible values are: png, png:cairo, png:cairo:cairo, png:cairo:gd, png:gd,
+# png:gd:gd, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd, gif, gif:cairo,
+# gif:cairo:gd, gif:gd, gif:gd:gd and svg.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: 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.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 INTERACTIVE_SVG        = NO
 
-# The tag DOT_PATH can be used to specify the path where the dot tool can be
+# The DOT_PATH tag 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.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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).
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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).
+# 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.
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_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.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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
+# 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.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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).
+# 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).
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the DOT_CLEANUP tag is set to YES doxygen will remove the intermediate dot
+# files that are used to generate the various graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 DOT_CLEANUP            = YES
Binary file Resources/OrthancLogo.png has changed
Binary file Resources/OrthancLogoDocumentation.png has changed
--- a/Resources/OrthancPlugin.doxygen	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/OrthancPlugin.doxygen	Fri Oct 12 15:18:10 2018 +0200
@@ -1,110 +1,129 @@
-# Doxyfile 1.8.1.2
+# Doxyfile 1.8.7
 
 # 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.
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single 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 (" ").
+# 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.
+# 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.
+# The default value is: UTF-8.
 
 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.
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
 
 PROJECT_NAME           = "Orthanc Plugin SDK"
 
-# 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         =
+# 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         = @ORTHANC_VERSION@
 
 # 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.
+# 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          = "Documentation of the plugin interface of Orthanc"
 
-# 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.
+# 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           = @CMAKE_SOURCE_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.
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. 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       = OrthancPluginDocumentation
 
-# 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.
+# 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 causes
+# performance problems for the file system.
+# The default value is: NO.
 
 CREATE_SUBDIRS         = NO
 
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES    = 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.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
 
 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.
+# If the BRIEF_MEMBER_DESC tag is set to YES 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.
+# The default value is: YES.
 
 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
+# If the REPEAT_BRIEF tag is set to YES 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.
+# The default value is: YES.
 
 REPEAT_BRIEF           = NO
 
-# 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"
+# 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 and 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
+# doxygen will generate a detailed section even if there is only a brief
 # description.
+# The default value is: NO.
 
 ALWAYS_DETAILED_SEC    = NO
 
@@ -112,169 +131,207 @@
 # 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.
+# The default value is: NO.
 
 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.
+# If the FULL_PATH_NAMES tag is set to YES 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
+# The default value is: YES.
 
 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.
+# 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.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
 
 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.
+# 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 list of 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.
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
 
 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.)
+# 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-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
 
 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.)
+# 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 Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
 
 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.
+# 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 behavior. 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 behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
 
 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.
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
 
 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.
+# 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.
+# The default value is: NO.
 
 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.
+# 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.
+# Minimum value: 1, maximum value: 16, default value: 4.
 
 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.
+# This tag can be used to specify a number of aliases that act 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.
+# 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.
+# 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.
+# The default value is: NO.
 
 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.
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
 
 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.
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
 
 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.
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
 
 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.
+# 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,
+# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran:
+# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran:
+# Fortran. In the later case the parser tries to guess whether the code is fixed
+# or free formatted code, this is the default for Fortran type files), VHDL. 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 For files without extension you can use no_extension as a placeholder.
+#
+# 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
+# If the MARKDOWN_SUPPORT tag is enabled 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.
+# 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.
+# The default value is: YES.
 
 MARKDOWN_SUPPORT       = YES
 
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by by putting a % sign in front of the word
+# or globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_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
+# 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);
+# versus func(std::string) {}). This also make the inheritance and collaboration
 # diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
 
 BUILTIN_STL_SUPPORT    = YES
 
 # If you use Microsoft's C++/CLI language, you should set this option to YES to
 # enable parsing support.
+# The default value is: NO.
 
 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.
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# http://www.riverbankcomputing.co.uk/software/sip/intro) 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.
+# The default value is: NO.
 
 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.
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to 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.
+# The default value is: YES.
 
 IDL_PROPERTY_SUPPORT   = YES
 
@@ -282,67 +339,61 @@
 # 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.
+# The default value is: NO.
 
 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.
+# Set the SUBGROUPING tag to YES 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.
+# The default value is: YES.
 
 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).
+# 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).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
 
 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).
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef 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, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
 
 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
+# When TYPEDEF_HIDES_STRUCT tag 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
+# 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.
+# The default value is: NO.
 
 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.
+# 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 appears 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. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
 
 LOOKUP_CACHE_SIZE      = 0
 
@@ -351,341 +402,391 @@
 #---------------------------------------------------------------------------
 
 # 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
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
 
 EXTRACT_ALL            = NO
 
-# If the EXTRACT_PRIVATE tag is set to YES all private members of a class
-# will be included in the documentation.
+# If the EXTRACT_PRIVATE tag is set to YES all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
 
 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.
+# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
 
 EXTRACT_PACKAGE        = NO
 
-# If the EXTRACT_STATIC tag is set to YES all static members of a file
-# will be included in the documentation.
+# If the EXTRACT_STATIC tag is set to YES all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
 
 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.
+# 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. Does not have any effect
+# for Java sources.
+# The default value is: YES.
 
 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.
+# 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 only methods in the interface are
+# included.
+# The default value is: NO.
 
 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.
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
 
 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.
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO 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.
+# The default value is: NO.
 
 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.
+# 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 these classes will be included in the various overviews. This option has
+# no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
 
 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.
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# (class|struct|union) declarations. If set to NO these declarations will be
+# included in the documentation.
+# The default value is: NO.
 
 HIDE_FRIEND_COMPOUNDS  = YES
 
-# 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.
+# 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 these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
 
 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.
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
 
 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
+# 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.
+# The default value is: system dependent.
 
 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.
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES the
+# scope will be hidden.
+# The default value is: NO.
 
 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.
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
 
 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.
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC  = NO
+
+# 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.
+# The default value is: NO.
 
 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.
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
 
 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.
+# If the SORT_MEMBER_DOCS tag is set to YES 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.
+# The default value is: YES.
 
 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.
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
 
 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.
+# 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 constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: 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.
+# 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 group names will
+# appear in their defined order.
+# The default value is: NO.
 
 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.
+# 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 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.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
 
 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.
+# 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.
+# The default value is: NO.
 
 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.
+# 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.
+# The default value is: YES.
 
 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.
+# 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.
+# The default value is: YES.
 
 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.
+# 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.
+# The default value is: YES.
 
 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.
+# 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.
+# The default value is: YES.
 
 GENERATE_DEPRECATEDLIST= YES
 
-# The ENABLED_SECTIONS tag can be used to enable conditional
-# documentation sections, marked by \if sectionname ... \endif.
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
 
 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.
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have 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 value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
 
 MAX_INITIALIZER_LINES  = 0
 
-# 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.
+# 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.
+# The default value is: YES.
 
 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.
+# 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 value 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.
+# 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 value 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.
+# 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. For an example see the documentation.
 
 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.
+# 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.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
 
 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.
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This 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. Do not use file names with spaces, bibtex cannot handle them. See
+# also \cite for info how to create references.
 
 CITE_BIB_FILES         =
 
 #---------------------------------------------------------------------------
-# configuration options related to warning and progress messages
+# 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.
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
 
 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.
+# generated to standard error ( stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
 
 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.
+# If the WARN_IF_UNDOCUMENTED tag 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.
+# The default value is: YES.
 
 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.
+# If the WARN_IF_DOC_ERROR tag 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.
+# The default value is: YES.
 
 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.
+# This 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 doxygen will only warn about wrong or incomplete parameter
+# documentation, but not about the absence of documentation.
+# The default value is: NO.
 
 WARN_NO_PARAMDOC       = YES
 
-# 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)
+# 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)
+# The default value is: $file:$line: $text.
 
 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.
+# 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 standard
+# error (stderr).
 
 WARN_LOGFILE           =
 
 #---------------------------------------------------------------------------
-# configuration options related to the input files
+# 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.
+# The INPUT tag is 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.
+# Note: If this tag is empty the current directory is searched.
 
 INPUT                  = @CMAKE_SOURCE_DIR@/Plugins/Include/orthanc/OrthancCPlugin.h \
-                         @CMAKE_SOURCE_DIR@/Plugins/Include/orthanc/OrthancCDatabasePlugin.h \
-                         @CMAKE_SOURCE_DIR@/Plugins/Include/orthanc/OrthancCppDatabasePlugin.h
+                         @CMAKE_SOURCE_DIR@/Plugins/Include/orthanc/OrthancCDatabasePlugin.h
 
 # 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.
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see: http://www.gnu.org/software/libiconv) for the list of
+# possible encodings.
+# The default value is: UTF-8.
 
 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 tag to specify one or more wildcard patterns (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++, *.java, *.ii,
+# *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp,
+# *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown,
+# *.md, *.mm, *.dox, *.py, *.f90, *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf,
+# *.qsf, *.as and *.js.
 
 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.
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
 
 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.
 
@@ -694,14 +795,16 @@
 # 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.
+# The default value is: NO.
 
 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/*
+# 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       =
 
@@ -710,755 +813,1077 @@
 # 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        = _OrthancPlugin* OrthancPluginGetName
-
-# 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).
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS        = _OrthancPlugin* \
+                         OrthancPluginGetName
+
+# 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 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.
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
 
 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).
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be 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.
+# 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.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
 
 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.
+# 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 information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none 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).
+# INPUT_FILTER ) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
 
 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.
+# 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 tag requires that the tag FILTER_SOURCE_FILES is set to YES.
 
 FILTER_SOURCE_PATTERNS =
 
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE =
+
 #---------------------------------------------------------------------------
-# configuration options related to source browsing
+# 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.
+# 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 that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
 
 SOURCE_BROWSER         = NO
 
-# Setting the INLINE_SOURCES tag to YES will include the body
-# of functions and classes directly in the documentation.
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
 
 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.
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
 
 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.
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# function all documented functions referencing it will be listed.
+# The default value is: NO.
 
 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.
+# 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.
+# The default value is: NO.
 
 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.
+# If the REFERENCES_LINK_SOURCE tag is set to YES 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.
+# The default value is: YES.
 
 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.
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS        = 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.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the config file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
 
 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.
+# If the VERBATIM_HEADERS tag is set the YES 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.
+# See also: Section \class.
+# The default value is: YES.
 
 VERBATIM_HEADERS       = YES
 
 #---------------------------------------------------------------------------
-# configuration options related to the alphabetical class index
+# 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.
+# 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.
+# The default value is: YES.
 
 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])
+# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in
+# which the alphabetical index list will be split.
+# Minimum value: 1, maximum value: 20, default value: 5.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
 
 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.
+# 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 a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
 
 IGNORE_PREFIX          =
 
 #---------------------------------------------------------------------------
-# configuration options related to the HTML output
+# Configuration options related to the HTML output
 #---------------------------------------------------------------------------
 
-# If the GENERATE_HTML tag is set to YES (the default) Doxygen will
-# generate HTML output.
+# If the GENERATE_HTML tag is set to YES doxygen will generate HTML output
+# The default value is: YES.
 
 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.
+# 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.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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!
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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!
+# 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 left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 HTML_STYLESHEET        =
 
+# The HTML_EXTRA_STYLESHEET tag can be used to specify an additional user-
+# defined cascading style sheet that is included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefor more robust against future updates.
+# Doxygen will copy the style sheet file to the output directory. For an example
+# see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_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.
+# $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.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the stylesheet 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.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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
+# 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 (see: http://developer.apple.com/tools/xcode/), 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# This tag determines the name of the docset 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.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
 
 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.
+# 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.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
 
 DOCSET_BUNDLE_ID       = org.doxygen.Project
 
-# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely identify
+# The DOCSET_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.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
 
 DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
 
-# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher.
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
 
 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.
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on
+# Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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
+# 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.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
 
 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.
+# 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.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
 
 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).
+# 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).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
 
 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.
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index ( hhk), content ( hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
 
 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.
+# 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. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
 
 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.
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual-
+# folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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>.
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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>.
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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.
+# 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.
+# This tag requires that the tag GENERATE_QHP is set to YES.
 
 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.
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
 
 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.
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set 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.
+# 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. Via custom stylesheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values 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.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. 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.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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 directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 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.
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT         = HTML-CSS
+
+# 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.
+# The default value is: http://cdn.mathjax.org/mathjax/latest.
+# This tag requires that the tag USE_MATHJAX is set to YES.
 
 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.
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
 
 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.
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE       =
+
+# 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. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
 
 SEARCHENGINE           = NO
 
 # 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.
+# implemented using a web server instead of a web client using Javascript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
 
 SERVER_BASED_SEARCH    = NO
 
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer ( doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH        = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer ( doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/). See the section "External Indexing and
+# Searching" for details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL       =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE        = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID     =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS  =
+
 #---------------------------------------------------------------------------
-# configuration options related to the LaTeX output
+# Configuration options related to the LaTeX output
 #---------------------------------------------------------------------------
 
-# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will
-# generate Latex output.
+# If the GENERATE_LATEX tag is set to YES doxygen will generate LaTeX output.
+# The default value is: YES.
 
 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.
+# 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.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# invoked.
+#
+# 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.
+# The default file is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. To get the times font for
+# instance you can specify
+# EXTRA_PACKAGES=times
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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!
+# 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. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber. Doxygen will
+# replace them by respectively the title of the page, the current date and time,
+# only the current date, the version number of doxygen, the project name (see
+# PROJECT_NAME), or the project number (see PROJECT_NUMBER).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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!
+# 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.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES      =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). 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.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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
+# If the LATEX_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
+# the PDF file directly from the LaTeX files. Set this option to YES to get a
 # higher quality PDF documentation.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# If the LATEX_SOURCE_CODE tag 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 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.
+# bibliography, e.g. plainnat, or ieeetr. See
+# http://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
 
 LATEX_BIB_STYLE        = plain
 
 #---------------------------------------------------------------------------
-# configuration options related to the RTF output
+# 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.
+# 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 too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
 
 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.
+# 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.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
 
 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.
+# 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 some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
 
 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.
+# Load stylesheet 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.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
 
 RTF_STYLESHEET_FILE    =
 
-# Set optional variables used in the generation of an rtf document.
-# Syntax is similar to doxygen's config file.
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's config file. A template extensions file can be generated
+# using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
 
 RTF_EXTENSIONS_FILE    =
 
 #---------------------------------------------------------------------------
-# configuration options related to the man page output
+# Configuration options related to the man page output
 #---------------------------------------------------------------------------
 
-# If the GENERATE_MAN tag is set to YES (the default) Doxygen will
-# generate man pages
+# If the GENERATE_MAN tag is set to YES doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
 
 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.
+# 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. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
 
 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)
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
 
 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.
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR             =
+
+# 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 value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
 
 MAN_LINKS              = NO
 
 #---------------------------------------------------------------------------
-# configuration options related to the XML output
+# 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.
+# 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.
+# The default value is: NO.
 
 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.
+# 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.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
 
 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.
+# 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.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
 
 XML_PROGRAMLISTING     = YES
 
 #---------------------------------------------------------------------------
-# configuration options for the AutoGen Definitions output
+# Configuration options related to the DOCBOOK 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.
+# If the GENERATE_DOCBOOK tag is set to YES doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK       = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT         = docbook
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES doxygen will generate an AutoGen
+# Definitions (see http://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.
+# The default value is: NO.
 
 GENERATE_AUTOGEN_DEF   = NO
 
 #---------------------------------------------------------------------------
-# configuration options related to the Perl module output
+# 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.
+# 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.
+# The default value is: NO.
 
 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
 
 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.
+# 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.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
 
 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.
+# 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.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
 
 PERLMOD_MAKEVAR_PREFIX =
 
@@ -1466,106 +1891,128 @@
 # 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.
+# If the ENABLE_PREPROCESSING tag is set to YES doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
 
 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.
+# If the MACRO_EXPANSION tag is set to YES doxygen will expand all macro names
+# in the source code. If set to NO only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
 MACRO_EXPANSION        = YES
 
-# 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.
+# 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.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
 EXPAND_ONLY_PREDEF     = YES
 
-# 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.
+# If the SEARCH_INCLUDES tag is set to YES the includes files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
 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.
+# contain include files that are not input files but should be processed by the
+# preprocessor.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
 
 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.
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
 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.
+# 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 e.g.
+# 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.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
 PREDEFINED             = ORTHANC_PLUGIN_INLINE=
 
-# 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.
+# 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.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
 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.
+# If the SKIP_FUNCTION_MACROS tag is set to YES 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. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
 SKIP_FUNCTION_MACROS   = YES
 
 #---------------------------------------------------------------------------
-# Configuration::additions related to external references
+# Configuration options 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:
-#
+# The TAGFILES tag can be used to specify one or more tag files. 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.
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: 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.
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
 
 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.
+# If the ALLEXTERNALS tag is set to YES all external class will be listed in the
+# class index. If set to NO only the inherited external classes will be listed.
+# The default value is: NO.
 
 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.
+# 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.
+# The default value is: YES.
 
 EXTERNAL_GROUPS        = YES
 
+# If the EXTERNAL_PAGES tag is set to YES all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES         = YES
+
 # The PERL_PATH should be the absolute path and name of the perl script
-# interpreter (i.e. the result of `which perl').
+# interpreter (i.e. the result of 'which perl').
+# The default file (with absolute path) is: /usr/bin/perl.
 
 PERL_PATH              = /usr/bin/perl
 
@@ -1573,222 +2020,295 @@
 # 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.
+# If the CLASS_DIAGRAMS tag is set to YES doxygen will generate a class diagram
+# (in HTML 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.
+# The default value is: YES.
 
 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
+# 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.
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_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.
+# The default value is: YES.
 
 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)
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), 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 value is: YES.
 
 HAVE_DOT               = NO
 
-# 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.
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 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.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# When you want a differently looking font n the dot files that doxygen
+# generates 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.
+# The default value is: Helvetica.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
+# dot graphs.
+# Minimum value: 4, maximum value: 24, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# By default doxygen will tell dot to use the default font as specified with
+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
+# the path where dot can find it using this tag.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the CLASS_GRAPH tag is 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.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the COLLABORATION_GRAPH tag is 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.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 COLLABORATION_GRAPH    = YES
 
-# 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
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# 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 manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 UML_LIMIT_NUM_FIELDS   = 10
 
-# If set to YES, the inheritance and collaboration graphs will show the
-# relations between templates and their instances.
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES 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.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES 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.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the CALL_GRAPH tag is 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.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the CALLER_GRAPH tag is 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.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the DIRECTORY_GRAPH tag is 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.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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).
+# generated by dot.
+# Note: 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).
+# Possible values are: png, png:cairo, png:cairo:cairo, png:cairo:gd, png:gd,
+# png:gd:gd, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd, gif, gif:cairo,
+# gif:cairo:gd, gif:gd, gif:gd:gd and svg.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: 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.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 INTERACTIVE_SVG        = NO
 
-# The tag DOT_PATH can be used to specify the path where the dot tool can be
+# The DOT_PATH tag 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.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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).
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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).
+# 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.
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_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.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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
+# 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.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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).
+# 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).
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 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.
+# If the DOT_CLEANUP tag is set to YES doxygen will remove the intermediate dot
+# files that are used to generate the various graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
 
 DOT_CLEANUP            = YES
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/boost-1.65.1-linux-standard-base.patch	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,12 @@
+diff -urEb boost_1_65_1.orig/boost/move/adl_move_swap.hpp boost_1_65_1/boost/move/adl_move_swap.hpp
+--- boost_1_65_1.orig/boost/move/adl_move_swap.hpp	2017-11-08 17:43:20.000000000 +0100
++++ boost_1_65_1/boost/move/adl_move_swap.hpp	2018-01-02 15:34:48.829052917 +0100
+@@ -28,6 +28,8 @@
+ //Try to avoid including <algorithm>, as it's quite big
+ #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB)
+    #include <utility>   //Dinkum libraries define std::swap in utility which is lighter than algorithm
++#elif defined(__LSB_VERSION__)
++#  include <utility>
+ #elif defined(BOOST_GNU_STDLIB)
+    //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions
+    //use the good old stl_algobase header, which is quite lightweight
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/boost-1.66.0-linux-standard-base.patch	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,13 @@
+diff -urEb boost_1_66_0.orig/boost/move/adl_move_swap.hpp boost_1_66_0/boost/move/adl_move_swap.hpp
+--- boost_1_66_0.orig/boost/move/adl_move_swap.hpp	2018-04-11 11:56:16.761768726 +0200
++++ boost_1_66_0/boost/move/adl_move_swap.hpp	2018-04-11 11:57:01.073881330 +0200
+@@ -28,6 +28,8 @@
+ //Try to avoid including <algorithm>, as it's quite big
+ #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB)
+    #include <utility>   //Dinkum libraries define std::swap in utility which is lighter than algorithm
++#elif defined(__LSB_VERSION__)
++#  include <utility>
+ #elif defined(BOOST_GNU_STDLIB)
+    //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions
+    //use the good old stl_algobase header, which is quite lightweight
+Only in boost_1_66_0/boost/move: adl_move_swap.hpp~
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/boost-1.67.0-linux-standard-base.patch	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,74 @@
+diff -urEb boost_1_67_0.orig/boost/move/adl_move_swap.hpp boost_1_67_0/boost/move/adl_move_swap.hpp
+--- boost_1_67_0.orig/boost/move/adl_move_swap.hpp	2018-06-20 17:42:27.000000000 +0200
++++ boost_1_67_0/boost/move/adl_move_swap.hpp	2018-10-12 14:27:41.368076902 +0200
+@@ -28,6 +28,8 @@
+ //Try to avoid including <algorithm>, as it's quite big
+ #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB)
+    #include <utility>   //Dinkum libraries define std::swap in utility which is lighter than algorithm
++#elif defined(__LSB_VERSION__)
++#  include <utility>
+ #elif defined(BOOST_GNU_STDLIB)
+    //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions
+    //use the good old stl_algobase header, which is quite lightweight
+diff -urEb boost_1_67_0.orig/boost/thread/detail/config.hpp boost_1_67_0/boost/thread/detail/config.hpp
+--- boost_1_67_0.orig/boost/thread/detail/config.hpp	2018-06-20 17:42:27.000000000 +0200
++++ boost_1_67_0/boost/thread/detail/config.hpp	2018-10-12 14:27:41.372076898 +0200
+@@ -417,6 +417,8 @@
+   #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
+ #elif defined(BOOST_THREAD_CHRONO_MAC_API)
+   #define BOOST_THREAD_HAS_MONO_CLOCK
++#elif defined(__LSB_VERSION__) || defined(__ANDROID__)
++  #define BOOST_THREAD_HAS_MONO_CLOCK
+ #else
+   #include <time.h> // check for CLOCK_MONOTONIC
+   #if defined(CLOCK_MONOTONIC)
+diff -urEb boost_1_67_0.orig/boost/type_traits/detail/has_postfix_operator.hpp boost_1_67_0/boost/type_traits/detail/has_postfix_operator.hpp
+--- boost_1_67_0.orig/boost/type_traits/detail/has_postfix_operator.hpp	2018-06-20 17:42:27.000000000 +0200
++++ boost_1_67_0/boost/type_traits/detail/has_postfix_operator.hpp	2018-10-12 14:31:27.539874170 +0200
+@@ -32,8 +32,11 @@
+ namespace boost {
+ namespace detail {
+ 
++// https://stackoverflow.com/a/15474269
++#ifndef Q_MOC_RUN
+ // This namespace ensures that argument-dependent name lookup does not mess things up.
+ namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) {
++#endif
+ 
+ // 1. a function to have an instance of type T without requiring T to be default
+ // constructible
+@@ -181,7 +184,9 @@
+    BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Lhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value));
+ };
+ 
++#ifndef Q_MOC_RUN
+ } // namespace impl
++#endif
+ } // namespace detail
+ 
+ // this is the accessible definition of the trait to end user
+diff -urEb boost_1_67_0.orig/boost/type_traits/detail/has_prefix_operator.hpp boost_1_67_0/boost/type_traits/detail/has_prefix_operator.hpp
+--- boost_1_67_0.orig/boost/type_traits/detail/has_prefix_operator.hpp	2018-06-20 17:42:27.000000000 +0200
++++ boost_1_67_0/boost/type_traits/detail/has_prefix_operator.hpp	2018-10-12 14:31:40.991862281 +0200
+@@ -45,8 +45,11 @@
+ namespace boost {
+ namespace detail {
+ 
++// https://stackoverflow.com/a/15474269
++#ifndef Q_MOC_RUN
+ // This namespace ensures that argument-dependent name lookup does not mess things up.
+ namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) {
++#endif
+ 
+ // 1. a function to have an instance of type T without requiring T to be default
+ // constructible
+@@ -194,7 +197,9 @@
+    BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Rhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value));
+ };
+ 
++#ifndef Q_MOC_RUN
+ } // namespace impl
++#endif
+ } // namespace detail
+ 
+ // this is the accessible definition of the trait to end user
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/curl-7.57.0-cmake.patch	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,12 @@
+diff -urEb curl-7.57.0.orig/CMake/Macros.cmake curl-7.57.0/CMake/Macros.cmake
+--- curl-7.57.0.orig/CMake/Macros.cmake	2017-11-09 23:40:36.000000000 +0100
++++ curl-7.57.0/CMake/Macros.cmake	2018-01-03 10:39:15.589520034 +0100
+@@ -38,7 +38,7 @@
+     message(STATUS "Performing Curl Test ${CURL_TEST}")
+     try_compile(${CURL_TEST}
+       ${CMAKE_BINARY_DIR}
+-      ${CMAKE_CURRENT_SOURCE_DIR}/CMake/CurlTests.c
++      ${CURL_SOURCES_DIR}/CMake/CurlTests.c
+       CMAKE_FLAGS -DCOMPILE_DEFINITIONS:STRING=${MACRO_CHECK_FUNCTION_DEFINITIONS}
+       "${CURL_TEST_ADD_LIBRARIES}"
+       OUTPUT_VARIABLE OUTPUT)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/dcmtk-3.6.0-dulparse-vulnerability.patch	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,29 @@
+diff -urEb dcmtk-3.6.0.orig/dcmnet/libsrc/dulparse.cc dcmtk-3.6.0/dcmnet/libsrc/dulparse.cc
+--- dcmtk-3.6.0.orig/dcmnet/libsrc/dulparse.cc	2010-12-01 09:26:36.000000000 +0100
++++ dcmtk-3.6.0/dcmnet/libsrc/dulparse.cc	2016-12-02 15:58:49.930540033 +0100
+@@ -393,6 +393,8 @@
+                     return cond;
+ 
+                 buf += length;
++                if (presentationLength < length)
++                  return EC_MemoryExhausted;
+                 presentationLength -= length;
+                 DCMNET_TRACE("Successfully parsed Abstract Syntax");
+                 break;
+@@ -404,12 +406,16 @@
+                 cond = LST_Enqueue(&context->transferSyntaxList, (LST_NODE*)subItem);
+                 if (cond.bad()) return cond;
+                 buf += length;
++                if (presentationLength < length)
++                  return EC_MemoryExhausted;
+                 presentationLength -= length;
+                 DCMNET_TRACE("Successfully parsed Transfer Syntax");
+                 break;
+             default:
+                 cond = parseDummy(buf, &length, presentationLength);
+                 buf += length;
++                if (presentationLength < length)
++                  return EC_MemoryExhausted;
+                 presentationLength -= length;
+                 break;
+             }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/dcmtk-3.6.0-mingw64.patch	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,21 @@
+diff -urEb dcmtk-3.6.0.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.0/ofstd/include/dcmtk/ofstd/offile.h
+--- dcmtk-3.6.0.orig/ofstd/include/dcmtk/ofstd/offile.h	2010-12-17 11:50:30.000000000 +0100
++++ dcmtk-3.6.0/ofstd/include/dcmtk/ofstd/offile.h	2013-07-19 15:56:25.688996134 +0200
+@@ -196,7 +196,7 @@
+   OFBool popen(const char *command, const char *modes)
+   {
+     if (file_) fclose();
+-#ifdef _WIN32
++#if defined(_WIN32) && !defined(__MINGW64_VERSION_MAJOR)
+     file_ = _popen(command, modes);
+ #else
+     file_ = :: popen(command, modes);
+@@ -258,7 +258,7 @@
+     {
+       if (popened_)
+       {
+-#ifdef _WIN32
++#if defined(_WIN32) && !defined(__MINGW64_VERSION_MAJOR)
+         result = _pclose(file_);
+ #else
+         result = :: pclose(file_);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/dcmtk-3.6.0-speed.patch	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,54 @@
+diff -urEb dcmtk-3.6.0.orig/dcmnet/libsrc/dul.cc dcmtk-3.6.0/dcmnet/libsrc/dul.cc
+--- dcmtk-3.6.0.orig/dcmnet/libsrc/dul.cc	2017-03-17 15:49:23.043061969 +0100
++++ dcmtk-3.6.0/dcmnet/libsrc/dul.cc	2017-03-17 15:50:44.075359547 +0100
+@@ -630,7 +630,10 @@
+     if (cond.bad())
+         return cond;
+ 
+-    cond = PRV_NextPDUType(association, block, timeout, &pduType);
++    /* This is the first time we read from this new connection, so in case it
++     * doesn't speak DICOM, we shouldn't wait forever (= DUL_NOBLOCK).
++     */
++    cond = PRV_NextPDUType(association, DUL_NOBLOCK, PRV_DEFAULTTIMEOUT, &pduType);
+ 
+     if (cond == DUL_NETWORKCLOSED)
+         event = TRANS_CONN_CLOSED;
+@@ -1770,7 +1773,7 @@
+                 // send number of socket handle in child process over anonymous pipe
+                 DWORD bytesWritten;
+                 char buf[20];
+-                sprintf(buf, "%i", OFreinterpret_cast(int, childSocketHandle));
++                sprintf(buf, "%i", OFstatic_cast(int, OFreinterpret_cast(size_t, childSocketHandle)));
+                 if (!WriteFile(hChildStdInWriteDup, buf, strlen(buf) + 1, &bytesWritten, NULL))
+                 {
+                     CloseHandle(hChildStdInWriteDup);
+@@ -1780,7 +1783,7 @@
+                 // return OF_ok status code DULC_FORKEDCHILD with descriptive text
+                 OFOStringStream stream;
+                 stream << "New child process started with pid " << OFstatic_cast(int, pi.dwProcessId)
+-                       << ", socketHandle " << OFreinterpret_cast(int, childSocketHandle) << OFStringStream_ends;
++                       << ", socketHandle " << OFstatic_cast(int, OFreinterpret_cast(size_t, childSocketHandle)) << OFStringStream_ends;
+                 OFSTRINGSTREAM_GETOFSTRING(stream, msg)
+                 return makeDcmnetCondition(DULC_FORKEDCHILD, OF_ok, msg.c_str());
+             }
+@@ -1840,7 +1843,7 @@
+     }
+ #endif
+ #endif
+-    setTCPBufferLength(sock);
++    //setTCPBufferLength(sock);
+ 
+ #ifndef DONT_DISABLE_NAGLE_ALGORITHM
+     /*
+diff -urEb dcmtk-3.6.0.orig/dcmnet/libsrc/dulfsm.cc dcmtk-3.6.0/dcmnet/libsrc/dulfsm.cc
+--- dcmtk-3.6.0.orig/dcmnet/libsrc/dulfsm.cc	2017-03-17 15:49:23.043061969 +0100
++++ dcmtk-3.6.0/dcmnet/libsrc/dulfsm.cc	2017-03-17 15:49:48.467144792 +0100
+@@ -2417,7 +2417,7 @@
+           return makeDcmnetCondition(DULC_TCPINITERROR, OF_error, msg.c_str());
+         }
+ #endif
+-        setTCPBufferLength(s);
++        //setTCPBufferLength(s);
+ 
+ #ifndef DONT_DISABLE_NAGLE_ALGORITHM
+         /*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/dcmtk-3.6.2-cmath.patch	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,22 @@
+diff -urEb dcmtk-3.6.2.orig/CMake/GenerateDCMTKConfigure.cmake dcmtk-3.6.2/CMake/GenerateDCMTKConfigure.cmake
+--- dcmtk-3.6.2.orig/CMake/GenerateDCMTKConfigure.cmake	2018-09-20 09:30:34.364831213 +0200
++++ dcmtk-3.6.2/CMake/GenerateDCMTKConfigure.cmake	2018-09-20 09:47:52.013660067 +0200
+@@ -568,12 +568,12 @@
+   ENDIF(HAVE_CSTDDEF)
+ 
+   CHECK_FUNCTIONWITHHEADER_EXISTS(feenableexcept "${HEADERS}" HAVE_PROTOTYPE_FEENABLEEXCEPT)
+-  CHECK_FUNCTIONWITHHEADER_EXISTS(isinf "${HEADERS}" HAVE_PROTOTYPE_ISINF)
+-  CHECK_FUNCTIONWITHHEADER_EXISTS(isnan "${HEADERS}" HAVE_PROTOTYPE_ISNAN)
+-  CHECK_FUNCTIONWITHHEADER_EXISTS(finite "${HEADERS}" HAVE_PROTOTYPE_FINITE)
+-  CHECK_FUNCTIONWITHHEADER_EXISTS(std::isinf "${HEADERS}" HAVE_PROTOTYPE_STD__ISINF)
+-  CHECK_FUNCTIONWITHHEADER_EXISTS(std::isnan "${HEADERS}" HAVE_PROTOTYPE_STD__ISNAN)
+-  CHECK_FUNCTIONWITHHEADER_EXISTS(std::finite "${HEADERS}" HAVE_PROTOTYPE_STD__FINITE)
++  CHECK_FUNCTIONWITHHEADER_EXISTS("isinf(0.)" "${HEADERS}" HAVE_PROTOTYPE_ISINF)
++  CHECK_FUNCTIONWITHHEADER_EXISTS("isnan(0.)" "${HEADERS}" HAVE_PROTOTYPE_ISNAN)
++  CHECK_FUNCTIONWITHHEADER_EXISTS("finite(0.)" "${HEADERS}" HAVE_PROTOTYPE_FINITE)
++  CHECK_FUNCTIONWITHHEADER_EXISTS("std::isinf(0.)" "${HEADERS}" HAVE_PROTOTYPE_STD__ISINF)
++  CHECK_FUNCTIONWITHHEADER_EXISTS("std::isnan(0.)" "${HEADERS}" HAVE_PROTOTYPE_STD__ISNAN)
++  CHECK_FUNCTIONWITHHEADER_EXISTS("std::finite(0.)" "${HEADERS}" HAVE_PROTOTYPE_STD__FINITE)
+   CHECK_FUNCTIONWITHHEADER_EXISTS(flock "${HEADERS}" HAVE_PROTOTYPE_FLOCK)
+   CHECK_FUNCTIONWITHHEADER_EXISTS(gethostbyname "${HEADERS}" HAVE_PROTOTYPE_GETHOSTBYNAME)
+   CHECK_FUNCTIONWITHHEADER_EXISTS(gethostbyname_r "${HEADERS}" HAVE_PROTOTYPE_GETHOSTBYNAME_R)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/dcmtk-3.6.2-linux-standard-base.patch	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,12 @@
+diff -urEb dcmtk-3.6.2.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.2/ofstd/include/dcmtk/ofstd/offile.h
+--- dcmtk-3.6.2.orig/ofstd/include/dcmtk/ofstd/offile.h	2017-07-14 17:41:11.000000000 +0200
++++ dcmtk-3.6.2/ofstd/include/dcmtk/ofstd/offile.h	2018-01-02 13:56:04.075293459 +0100
+@@ -551,7 +551,7 @@
+    */
+   void setlinebuf()
+   {
+-#if defined(_WIN32) || defined(__hpux)
++#if defined(_WIN32) || defined(__hpux) || defined(__LSB_VERSION__)
+     this->setvbuf(NULL, _IOLBF, 0);
+ #else
+     :: setlinebuf(file_);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/dcmtk-3.6.2-private.dic	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,3040 @@
+#
+#  Copyright (C) 1994-2013, OFFIS e.V.
+#  All rights reserved.  See COPYRIGHT file for details.
+#
+#  This software and supporting documentation were developed by
+#
+#    OFFIS e.V.
+#    R&D Division Health
+#    Escherweg 2
+#    D-26121 Oldenburg, Germany
+#
+#
+#  Module:  dcmdata
+#
+#  Author:  Andrew Hewett, Marco Eichelberg, Joerg Riesmeier
+#
+#  Purpose:
+#  This is the private tag DICOM data dictionary for the dcmtk class library.
+#
+#
+# Dictionary of Private Tags
+#
+#  This dictionary contains the private tags defined in the following
+#  reference documents (in alphabetical order):
+#   - AGFA IMPAX 6.5.x Solution conformance statement
+#   - Circle Cardiovascular Imaging cmr42 3.0 conformance statement
+#   - David Clunie's dicom3tools package, 2002-04-20 snapshot
+#   - Fuji CR console, 3rd release
+#   - Intelerad Medical Systems Inc., Image Server
+#   - OCULUS Pentacam 1.17 conformance statement
+#   - Philips Digital Diagnost 1.3 conformance statement
+#   - Philips Integris H, catheterization laboratory, RIS-interface
+#   - Philips Intera Achieva conformance statement
+#   - Philips MR Achieva conformance statement
+#   - Siemens Somatom syngo VA40B conformance statement
+#   - Siemens AXIOM Artis VB30 conformance statement
+#   - SonoWand Invite 2.1.1 conformance statement
+#   - Swissvision TR4000 conformance statement
+#   - private tags for DCMTK anonymizer tool
+#
+# Each line represents an entry in the data dictionary.  Each line
+# has 5 fields (Tag, VR, Name, VM, Version).  Entries need not be
+# in ascending tag order.
+#
+# Entries may override existing entries.
+#
+# Each field must be separated by a single tab.
+# The tag value may take one of two forms:
+#   (gggg,"CREATOR",ee)
+#   (gggg,"CREATOR",eeee) [eeee >= 1000]
+# The first form describes a private tag that may be used with different
+# element numbers as reserved by the private creator element.
+# The second form describes a private tag that may only occur with a
+# certain fixed element number.
+# In both cases, the tag values must be in hexadecimal.
+# Repeating groups are represented by indicating the range
+# (gggg-o-gggg,"CREATOR",ee) or (gggg-o-gggg,"CREATOR",eeee)
+# where "-o-" indicates that only odd group numbers match the definition.
+# The element part of the tag can also be a range.
+#
+# Comments have a '#' at the beginning of the line.
+#
+# Tag				VR	Name			VM	Version / Description
+#
+(0019,"1.2.840.113681",10)	ST	CRImageParamsCommon	1	PrivateTag
+(0019,"1.2.840.113681",11)	ST	CRImageIPParamsSingle	1	PrivateTag
+(0019,"1.2.840.113681",12)	ST	CRImageIPParamsLeft	1	PrivateTag
+(0019,"1.2.840.113681",13)	ST	CRImageIPParamsRight	1	PrivateTag
+
+(0087,"1.2.840.113708.794.1.1.2.0",10)	CS	MediaType	1	PrivateTag
+(0087,"1.2.840.113708.794.1.1.2.0",20)	CS	MediaLocation	1	PrivateTag
+(0087,"1.2.840.113708.794.1.1.2.0",50)	IS	EstimatedRetrieveTime	1	PrivateTag
+
+(0009,"ACUSON",00)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",01)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",02)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",03)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",04)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",05)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",06)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",07)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",08)	LT	Unknown	1	PrivateTag
+(0009,"ACUSON",09)	LT	Unknown	1	PrivateTag
+(0009,"ACUSON",0a)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",0b)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",0c)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",0d)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",0e)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",0f)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",10)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",11)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",12)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",13)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",14)	LT	Unknown	1	PrivateTag
+(0009,"ACUSON",15)	UN	Unknown	1	PrivateTag
+
+(0003,"AEGIS_DICOM_2.00",00)	US	Unknown	1-n	PrivateTag
+(0005,"AEGIS_DICOM_2.00",00)	US	Unknown	1-n	PrivateTag
+(0009,"AEGIS_DICOM_2.00",00)	US	Unknown	1-n	PrivateTag
+(0019,"AEGIS_DICOM_2.00",00)	US	Unknown	1-n	PrivateTag
+(0029,"AEGIS_DICOM_2.00",00)	US	Unknown	1-n	PrivateTag
+(1369,"AEGIS_DICOM_2.00",00)	US	Unknown	1-n	PrivateTag
+
+(0009,"AGFA",10)	LO	Unknown	1	PrivateTag
+(0009,"AGFA",11)	LO	Unknown	1	PrivateTag
+(0009,"AGFA",13)	LO	Unknown	1	PrivateTag
+(0009,"AGFA",14)	LO	Unknown	1	PrivateTag
+(0009,"AGFA",15)	LO	Unknown	1	PrivateTag
+
+(0031,"AGFA PACS Archive Mirroring 1.0",00)	CS	StudyStatus	1	PrivateTag
+(0031,"AGFA PACS Archive Mirroring 1.0",01)	UL	DateTimeVerified	1	PrivateTag
+
+(0029,"CAMTRONICS IP",10)	LT	Unknown	1	PrivateTag
+(0029,"CAMTRONICS IP",20)	UN	Unknown	1	PrivateTag
+(0029,"CAMTRONICS IP",30)	UN	Unknown	1	PrivateTag
+(0029,"CAMTRONICS IP",40)	UN	Unknown	1	PrivateTag
+
+(0029,"CAMTRONICS",10)	LT	Commentline	1	PrivateTag
+(0029,"CAMTRONICS",20)	DS	EdgeEnhancementCoefficient	1	PrivateTag
+(0029,"CAMTRONICS",50)	LT	SceneText	1	PrivateTag
+(0029,"CAMTRONICS",60)	LT	ImageText	1	PrivateTag
+(0029,"CAMTRONICS",70)	IS	PixelShiftHorizontal	1	PrivateTag
+(0029,"CAMTRONICS",80)	IS	PixelShiftVertical	1	PrivateTag
+(0029,"CAMTRONICS",90)	IS	Unknown	1	PrivateTag
+
+(0009,"CARDIO-D.R. 1.0",00)	UL	FileLocation	1	PrivateTag
+(0009,"CARDIO-D.R. 1.0",01)	UL	FileSize	1	PrivateTag
+(0009,"CARDIO-D.R. 1.0",40)	SQ	AlternateImageSequence	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",00)	CS	ImageBlankingShape	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",02)	IS	ImageBlankingLeftVerticalEdge	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",04)	IS	ImageBlankingRightVerticalEdge	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",06)	IS	ImageBlankingUpperHorizontalEdge	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",08)	IS	ImageBlankingLowerHorizontalEdge	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",10)	IS	CenterOfCircularImageBlanking	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",12)	IS	RadiusOfCircularImageBlanking	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",30)	UL	MaximumImageFrameSize	1	PrivateTag
+(0021,"CARDIO-D.R. 1.0",13)	IS	ImageSequenceNumber	1	PrivateTag
+(0029,"CARDIO-D.R. 1.0",00)	SQ	EdgeEnhancementSequence	1	PrivateTag
+(0029,"CARDIO-D.R. 1.0",01)	US	ConvolutionKernelSize	2	PrivateTag
+(0029,"CARDIO-D.R. 1.0",02)	DS	ConvolutionKernelCoefficients	1-n	PrivateTag
+(0029,"CARDIO-D.R. 1.0",03)	DS	EdgeEnhancementGain	1	PrivateTag
+
+(0025,"CMR42 CIRCLECVI",1010)	LO	WorkspaceID	1	PrivateTag
+(0025,"CMR42 CIRCLECVI",1020)	LO	WorkspaceTimeString	1	PrivateTag
+(0025,"CMR42 CIRCLECVI",1030)	OB	WorkspaceStream	1	PrivateTag
+
+(0009,"DCMTK_ANONYMIZER",00)	SQ	AnonymizerUIDMap	1	PrivateTag
+(0009,"DCMTK_ANONYMIZER",10)	UI	AnonymizerUIDKey	1	PrivateTag
+(0009,"DCMTK_ANONYMIZER",20)	UI	AnonymizerUIDValue	1	PrivateTag
+(0009,"DCMTK_ANONYMIZER",30)	SQ	AnonymizerPatientIDMap	1	PrivateTag
+(0009,"DCMTK_ANONYMIZER",40)	LO	AnonymizerPatientIDKey	1	PrivateTag
+(0009,"DCMTK_ANONYMIZER",50)	LO	AnonymizerPatientIDValue	1	PrivateTag
+
+(0019,"DIDI TO PCR 1.1",22)	UN	RouteAET	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",23)	DS	PCRPrintScale	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",24)	UN	PCRPrintJobEnd	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",25)	IS	PCRNoFilmCopies	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",26)	IS	PCRFilmLayoutPosition	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",27)	UN	PCRPrintReportName	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",70)	UN	RADProtocolPrinter	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",71)	UN	RADProtocolMedium	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",90)	LO	UnprocessedFlag	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",91)	UN	KeyValues	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",92)	UN	DestinationPostprocessingFunction	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A0)	UN	Version	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A1)	UN	RangingMode	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A2)	UN	AbdomenBrightness	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A3)	UN	FixedBrightness	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A4)	UN	DetailContrast	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A5)	UN	ContrastBalance	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A6)	UN	StructureBoost	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A7)	UN	StructurePreference	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A8)	UN	NoiseRobustness	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A9)	UN	NoiseDoseLimit	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",AA)	UN	NoiseDoseStep	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",AB)	UN	NoiseFrequencyLimit	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",AC)	UN	WeakContrastLimit	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",AD)	UN	StrongContrastLimit	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",AE)	UN	StructureBoostOffset	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",AF)	UN	SmoothGain	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B0)	UN	MeasureField1	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B1)	UN	MeasureField2	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B2)	UN	KeyPercentile1	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B3)	UN	KeyPercentile2	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B4)	UN	DensityLUT	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B5)	UN	Brightness	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B6)	UN	Gamma	1	PrivateTag
+(0089,"DIDI TO PCR 1.1",10)	SQ	Unknown	1	PrivateTag
+
+(0029,"DIGISCAN IMAGE",31)	US	Unknown	1-n	PrivateTag
+(0029,"DIGISCAN IMAGE",32)	US	Unknown	1-n	PrivateTag
+(0029,"DIGISCAN IMAGE",33)	LT	Unknown	1	PrivateTag
+(0029,"DIGISCAN IMAGE",34)	LT	Unknown	1	PrivateTag
+
+(7001-o-70ff,"DLX_ANNOT_01",04)	ST	TextAnnotation	1	PrivateTag
+(7001-o-70ff,"DLX_ANNOT_01",05)	IS	Box	2	PrivateTag
+(7001-o-70ff,"DLX_ANNOT_01",07)	IS	ArrowEnd	2	PrivateTag
+
+(0015,"DLX_EXAMS_01",01)	DS	StenosisCalibrationRatio	1	PrivateTag
+(0015,"DLX_EXAMS_01",02)	DS	StenosisMagnification	1	PrivateTag
+(0015,"DLX_EXAMS_01",03)	DS	CardiacCalibrationRatio	1	PrivateTag
+
+(6001-o-60ff,"DLX_LKUP_01",01)	US	GrayPaletteColorLookupTableDescriptor	3	PrivateTag
+(6001-o-60ff,"DLX_LKUP_01",02)	US	GrayPaletteColorLookupTableData	1	PrivateTag
+
+(0011,"DLX_PATNT_01",01)	LT	PatientDOB	1	PrivateTag
+
+(0019,"DLX_SERIE_01",01)	DS	AngleValueLArm	1	PrivateTag
+(0019,"DLX_SERIE_01",02)	DS	AngleValuePArm	1	PrivateTag
+(0019,"DLX_SERIE_01",03)	DS	AngleValueCArm	1	PrivateTag
+(0019,"DLX_SERIE_01",04)	CS	AngleLabelLArm	1	PrivateTag
+(0019,"DLX_SERIE_01",05)	CS	AngleLabelPArm	1	PrivateTag
+(0019,"DLX_SERIE_01",06)	CS	AngleLabelCArm	1	PrivateTag
+(0019,"DLX_SERIE_01",07)	ST	ProcedureName	1	PrivateTag
+(0019,"DLX_SERIE_01",08)	ST	ExamName	1	PrivateTag
+(0019,"DLX_SERIE_01",09)	SH	PatientSize	1	PrivateTag
+(0019,"DLX_SERIE_01",0a)	IS	RecordView	1	PrivateTag
+(0019,"DLX_SERIE_01",10)	DS	InjectorDelay	1	PrivateTag
+(0019,"DLX_SERIE_01",11)	CS	AutoInject	1	PrivateTag
+(0019,"DLX_SERIE_01",14)	IS	AcquisitionMode	1	PrivateTag
+(0019,"DLX_SERIE_01",15)	CS	CameraRotationEnabled	1	PrivateTag
+(0019,"DLX_SERIE_01",16)	CS	ReverseSweep	1	PrivateTag
+(0019,"DLX_SERIE_01",17)	IS	SpatialFilterStrength	1	PrivateTag
+(0019,"DLX_SERIE_01",18)	IS	ZoomFactor	1	PrivateTag
+(0019,"DLX_SERIE_01",19)	IS	XZoomCenter	1	PrivateTag
+(0019,"DLX_SERIE_01",1a)	IS	YZoomCenter	1	PrivateTag
+(0019,"DLX_SERIE_01",1b)	DS	Focus	1	PrivateTag
+(0019,"DLX_SERIE_01",1c)	CS	Dose	1	PrivateTag
+(0019,"DLX_SERIE_01",1d)	IS	SideMark	1	PrivateTag
+(0019,"DLX_SERIE_01",1e)	IS	PercentageLandscape	1	PrivateTag
+(0019,"DLX_SERIE_01",1f)	DS	ExposureDuration	1	PrivateTag
+
+(00E1,"ELSCINT1",01)	US	DataDictionaryVersion	1	PrivateTag
+(00E1,"ELSCINT1",14)	LT	Unknown	1	PrivateTag
+(00E1,"ELSCINT1",22)	DS	Unknown	2	PrivateTag
+(00E1,"ELSCINT1",23)	DS	Unknown	2	PrivateTag
+(00E1,"ELSCINT1",24)	LT	Unknown	1	PrivateTag
+(00E1,"ELSCINT1",25)	LT	Unknown	1	PrivateTag
+(00E1,"ELSCINT1",40)	SH	OffsetFromCTMRImages	1	PrivateTag
+(0601,"ELSCINT1",00)	SH	ImplementationVersion	1	PrivateTag
+(0601,"ELSCINT1",20)	DS	RelativeTablePosition	1	PrivateTag
+(0601,"ELSCINT1",21)	DS	RelativeTableHeight	1	PrivateTag
+(0601,"ELSCINT1",30)	SH	SurviewDirection	1	PrivateTag
+(0601,"ELSCINT1",31)	DS	SurviewLength	1	PrivateTag
+(0601,"ELSCINT1",50)	SH	ImageViewType	1	PrivateTag
+(0601,"ELSCINT1",70)	DS	BatchNumber	1	PrivateTag
+(0601,"ELSCINT1",71)	DS	BatchSize	1	PrivateTag
+(0601,"ELSCINT1",72)	DS	BatchSliceNumber	1	PrivateTag
+
+(0009,"FDMS 1.0",04)	SH	ImageControlUnit	1	PrivateTag
+(0009,"FDMS 1.0",05)	OW	ImageUID	1	PrivateTag
+(0009,"FDMS 1.0",06)	OW	RouteImageUID	1	PrivateTag
+(0009,"FDMS 1.0",08)	UL	ImageDisplayInformationVersionNo	1	PrivateTag
+(0009,"FDMS 1.0",09)	UL	PatientInformationVersionNo	1	PrivateTag
+(0009,"FDMS 1.0",0C)	OW	FilmUID	1	PrivateTag
+(0009,"FDMS 1.0",10)	CS	ExposureUnitTypeCode	1	PrivateTag
+(0009,"FDMS 1.0",80)	LO	KanjiHospitalName	1	PrivateTag
+(0009,"FDMS 1.0",90)	ST	DistributionCode	1	PrivateTag
+(0009,"FDMS 1.0",92)	SH	KanjiDepartmentName	1	PrivateTag
+(0009,"FDMS 1.0",F0)	CS	BlackeningProcessFlag	1	PrivateTag
+(0019,"FDMS 1.0",15)	LO	KanjiBodyPartForExposure	1	PrivateTag
+(0019,"FDMS 1.0",32)	LO	KanjiMenuName	1	PrivateTag
+(0019,"FDMS 1.0",40)	CS	ImageProcessingType	1	PrivateTag
+(0019,"FDMS 1.0",50)	CS	EDRMode	1	PrivateTag
+(0019,"FDMS 1.0",60)	SH	RadiographersCode	1	PrivateTag
+(0019,"FDMS 1.0",70)	IS	SplitExposureFormat	1	PrivateTag
+(0019,"FDMS 1.0",71)	IS	NoOfSplitExposureFrames	1	PrivateTag
+(0019,"FDMS 1.0",80)	IS	ReadingPositionSpecification	1	PrivateTag
+(0019,"FDMS 1.0",81)	IS	ReadingSensitivityCenter	1	PrivateTag
+(0019,"FDMS 1.0",90)	SH	FilmAnnotationCharacterString1	1	PrivateTag
+(0019,"FDMS 1.0",91)	SH	FilmAnnotationCharacterString2	1	PrivateTag
+(0021,"FDMS 1.0",10)	CS	FCRImageID	1	PrivateTag
+(0021,"FDMS 1.0",30)	CS	SetNo	1	PrivateTag
+(0021,"FDMS 1.0",40)	IS	ImageNoInTheSet	1	PrivateTag
+(0021,"FDMS 1.0",50)	CS	PairProcessingInformation	1	PrivateTag
+(0021,"FDMS 1.0",80)	OB	EquipmentTypeSpecificInformation	1	PrivateTag
+(0023,"FDMS 1.0",10)	SQ	Unknown	1	PrivateTag
+(0023,"FDMS 1.0",20)	SQ	Unknown	1	PrivateTag
+(0023,"FDMS 1.0",30)	SQ	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",10)	US	RelativeLightEmissionAmountSk	1	PrivateTag
+(0025,"FDMS 1.0",11)	US	TermOfCorrectionForEachIPTypeSt	1	PrivateTag
+(0025,"FDMS 1.0",12)	US	ReadingGainGp	1	PrivateTag
+(0025,"FDMS 1.0",13)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",15)	CS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",20)	US	Unknown	2	PrivateTag
+(0025,"FDMS 1.0",21)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",30)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",31)	SS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",32)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",33)	SS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",34)	SS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",40)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",41)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",42)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",43)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",50)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",51)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",52)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",53)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",60)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",61)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",62)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",63)	CS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",70)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",71)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",72)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",73)	US	Unknown	1-n	PrivateTag
+(0025,"FDMS 1.0",74)	US	Unknown	1-n	PrivateTag
+(0025,"FDMS 1.0",80)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",81)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",82)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",83)	US	Unknown	1-n	PrivateTag
+(0025,"FDMS 1.0",84)	US	Unknown	1-n	PrivateTag
+(0025,"FDMS 1.0",90)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",91)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",92)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",93)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",94)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",95)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",96)	CS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",a0)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",a1)	SS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",a2)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",a3)	SS	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",10)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",20)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",30)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",40)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",50)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",60)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",70)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",80)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",a0)	IS	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",a1)	CS	Unknown	2	PrivateTag
+(0027,"FDMS 1.0",a2)	CS	Unknown	2	PrivateTag
+(0027,"FDMS 1.0",a3)	SS	Unknown	1-n	PrivateTag
+(0029,"FDMS 1.0",20)	CS	ImageScanningDirection	1	PrivateTag
+(0029,"FDMS 1.0",30)	CS	ExtendedReadingSizeValue	1	PrivateTag
+(0029,"FDMS 1.0",34)	US	MagnificationReductionRatio	1	PrivateTag
+(0029,"FDMS 1.0",44)	CS	LineDensityCode	1	PrivateTag
+(0029,"FDMS 1.0",50)	CS	DataCompressionCode	1	PrivateTag
+(2011,"FDMS 1.0",11)	CS	ImagePosition SpecifyingFlag	1	PrivateTag
+(50F1,"FDMS 1.0",06)	CS	EnergySubtractionParam	1	PrivateTag
+(50F1,"FDMS 1.0",07)	CS	SubtractionRegistrationResult	1	PrivateTag
+(50F1,"FDMS 1.0",08)	CS	EnergySubtractionParam2	1	PrivateTag
+(50F1,"FDMS 1.0",09)	SL	AfinConversionCoefficient	1	PrivateTag
+(50F1,"FDMS 1.0",10)	CS	FilmOutputFormat	1	PrivateTag
+(50F1,"FDMS 1.0",20)	CS	ImageProcessingModificationFlag	1	PrivateTag
+
+(0009,"FFP DATA",01)	UN	CRHeaderInformation	1	PrivateTag
+
+(0019,"GE ??? From Adantage Review CS",30)	LO	CREDRMode	1	PrivateTag
+(0019,"GE ??? From Adantage Review CS",40)	LO	CRLatitude	1	PrivateTag
+(0019,"GE ??? From Adantage Review CS",50)	LO	CRGroupNumber	1	PrivateTag
+(0019,"GE ??? From Adantage Review CS",70)	LO	CRImageSerialNumber	1	PrivateTag
+(0019,"GE ??? From Adantage Review CS",80)	LO	CRBarCodeNumber	1	PrivateTag
+(0019,"GE ??? From Adantage Review CS",90)	LO	CRFilmOutputExposures	1	PrivateTag
+
+(0009,"GEMS_ACQU_01",24)	DS	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",25)	US	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",3e)	US	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",3f)	US	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",42)	US	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",43)	US	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",f8)	US	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",fb)	IS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",01)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",02)	SL	NumberOfCellsInDetector	1	PrivateTag
+(0019,"GEMS_ACQU_01",03)	DS	CellNumberAtTheta	1	PrivateTag
+(0019,"GEMS_ACQU_01",04)	DS	CellSpacing	1	PrivateTag
+(0019,"GEMS_ACQU_01",05)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",06)	UN	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",0e)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",0f)	DS	HorizontalFrameOfReference	1	PrivateTag
+(0019,"GEMS_ACQU_01",11)	SS	SeriesContrast	1	PrivateTag
+(0019,"GEMS_ACQU_01",12)	SS	LastPseq	1	PrivateTag
+(0019,"GEMS_ACQU_01",13)	SS	StartNumberForBaseline	1	PrivateTag
+(0019,"GEMS_ACQU_01",14)	SS	End NumberForBaseline	1	PrivateTag
+(0019,"GEMS_ACQU_01",15)	SS	StartNumberForEnhancedScans	1	PrivateTag
+(0019,"GEMS_ACQU_01",16)	SS	EndNumberForEnhancedScans	1	PrivateTag
+(0019,"GEMS_ACQU_01",17)	SS	SeriesPlane	1	PrivateTag
+(0019,"GEMS_ACQU_01",18)	LO	FirstScanRAS	1	PrivateTag
+(0019,"GEMS_ACQU_01",19)	DS	FirstScanLocation	1	PrivateTag
+(0019,"GEMS_ACQU_01",1a)	LO	LastScanRAS	1	PrivateTag
+(0019,"GEMS_ACQU_01",1b)	DS	LastScanLocation	1	PrivateTag
+(0019,"GEMS_ACQU_01",1e)	DS	DisplayFieldOfView	1	PrivateTag
+(0019,"GEMS_ACQU_01",20)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",22)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",23)	DS	TableSpeed	1	PrivateTag
+(0019,"GEMS_ACQU_01",24)	DS	MidScanTime	1	PrivateTag
+(0019,"GEMS_ACQU_01",25)	SS	MidScanFlag	1	PrivateTag
+(0019,"GEMS_ACQU_01",26)	SL	DegreesOfAzimuth	1	PrivateTag
+(0019,"GEMS_ACQU_01",27)	DS	GantryPeriod	1	PrivateTag
+(0019,"GEMS_ACQU_01",2a)	DS	XrayOnPosition	1	PrivateTag
+(0019,"GEMS_ACQU_01",2b)	DS	XrayOffPosition	1	PrivateTag
+(0019,"GEMS_ACQU_01",2c)	SL	NumberOfTriggers	1	PrivateTag
+(0019,"GEMS_ACQU_01",2d)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",2e)	DS	AngleOfFirstView	1	PrivateTag
+(0019,"GEMS_ACQU_01",2f)	DS	TriggerFrequency	1	PrivateTag
+(0019,"GEMS_ACQU_01",39)	SS	ScanFOVType	1	PrivateTag
+(0019,"GEMS_ACQU_01",3a)	IS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",3b)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",3c)	UN	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",3e)	UN	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",3f)	UN	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",40)	SS	StatReconFlag	1	PrivateTag
+(0019,"GEMS_ACQU_01",41)	SS	ComputeType	1	PrivateTag
+(0019,"GEMS_ACQU_01",42)	SS	SegmentNumber	1	PrivateTag
+(0019,"GEMS_ACQU_01",43)	SS	TotalSegmentsRequested	1	PrivateTag
+(0019,"GEMS_ACQU_01",44)	DS	InterscanDelay	1	PrivateTag
+(0019,"GEMS_ACQU_01",47)	SS	ViewCompressionFactor	1	PrivateTag
+(0019,"GEMS_ACQU_01",48)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",49)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",4a)	SS	TotalNumberOfRefChannels	1	PrivateTag
+(0019,"GEMS_ACQU_01",4b)	SL	DataSizeForScanData	1	PrivateTag
+(0019,"GEMS_ACQU_01",52)	SS	ReconPostProcessingFlag	1	PrivateTag
+(0019,"GEMS_ACQU_01",54)	UN	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",57)	SS	CTWaterNumber	1	PrivateTag
+(0019,"GEMS_ACQU_01",58)	SS	CTBoneNumber	1	PrivateTag
+(0019,"GEMS_ACQU_01",5a)	FL	AcquisitionDuration	1	PrivateTag
+(0019,"GEMS_ACQU_01",5d)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",5e)	SL	NumberOfChannels1To512	1	PrivateTag
+(0019,"GEMS_ACQU_01",5f)	SL	IncrementBetweenChannels	1	PrivateTag
+(0019,"GEMS_ACQU_01",60)	SL	StartingView	1	PrivateTag
+(0019,"GEMS_ACQU_01",61)	SL	NumberOfViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",62)	SL	IncrementBetweenViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",6a)	SS	DependantOnNumberOfViewsProcessed	1	PrivateTag
+(0019,"GEMS_ACQU_01",6b)	SS	FieldOfViewInDetectorCells	1	PrivateTag
+(0019,"GEMS_ACQU_01",70)	SS	ValueOfBackProjectionButton	1	PrivateTag
+(0019,"GEMS_ACQU_01",71)	SS	SetIfFatqEstimatesWereUsed	1	PrivateTag
+(0019,"GEMS_ACQU_01",72)	DS	ZChannelAvgOverViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",73)	DS	AvgOfLeftRefChannelsOverViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",74)	DS	MaxLeftChannelOverViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",75)	DS	AvgOfRightRefChannelsOverViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",76)	DS	MaxRightChannelOverViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",7d)	DS	SecondEcho	1	PrivateTag
+(0019,"GEMS_ACQU_01",7e)	SS	NumberOfEchos	1	PrivateTag
+(0019,"GEMS_ACQU_01",7f)	DS	TableDelta	1	PrivateTag
+(0019,"GEMS_ACQU_01",81)	SS	Contiguous	1	PrivateTag
+(0019,"GEMS_ACQU_01",82)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",83)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",84)	DS	PeakSAR	1	PrivateTag
+(0019,"GEMS_ACQU_01",85)	SS	MonitorSAR	1	PrivateTag
+(0019,"GEMS_ACQU_01",86)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",87)	DS	CardiacRepetition Time	1	PrivateTag
+(0019,"GEMS_ACQU_01",88)	SS	ImagesPerCardiacCycle	1	PrivateTag
+(0019,"GEMS_ACQU_01",8a)	SS	ActualReceiveGainAnalog	1	PrivateTag
+(0019,"GEMS_ACQU_01",8b)	SS	ActualReceiveGainDigital	1	PrivateTag
+(0019,"GEMS_ACQU_01",8d)	DS	DelayAfterTrigger	1	PrivateTag
+(0019,"GEMS_ACQU_01",8f)	SS	SwapPhaseFrequency	1	PrivateTag
+(0019,"GEMS_ACQU_01",90)	SS	PauseInterval	1	PrivateTag
+(0019,"GEMS_ACQU_01",91)	DS	PulseTime	1	PrivateTag
+(0019,"GEMS_ACQU_01",92)	SL	SliceOffsetOnFrequencyAxis	1	PrivateTag
+(0019,"GEMS_ACQU_01",93)	DS	CenterFrequency	1	PrivateTag
+(0019,"GEMS_ACQU_01",94)	SS	TransmitGain	1	PrivateTag
+(0019,"GEMS_ACQU_01",95)	SS	AnalogReceiverGain	1	PrivateTag
+(0019,"GEMS_ACQU_01",96)	SS	DigitalReceiverGain	1	PrivateTag
+(0019,"GEMS_ACQU_01",97)	SL	BitmapDefiningCVs	1	PrivateTag
+(0019,"GEMS_ACQU_01",98)	SS	CenterFrequencyMethod	1	PrivateTag
+(0019,"GEMS_ACQU_01",99)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",9b)	SS	PulseSequenceMode	1	PrivateTag
+(0019,"GEMS_ACQU_01",9c)	LO	PulseSequenceName	1	PrivateTag
+(0019,"GEMS_ACQU_01",9d)	DT	PulseSequenceDate	1	PrivateTag
+(0019,"GEMS_ACQU_01",9e)	LO	InternalPulseSequenceName	1	PrivateTag
+(0019,"GEMS_ACQU_01",9f)	SS	TransmittingCoil	1	PrivateTag
+(0019,"GEMS_ACQU_01",a0)	SS	SurfaceCoilType	1	PrivateTag
+(0019,"GEMS_ACQU_01",a1)	SS	ExtremityCoilFlag	1	PrivateTag
+(0019,"GEMS_ACQU_01",a2)	SL	RawDataRunNumber	1	PrivateTag
+(0019,"GEMS_ACQU_01",a3)	UL	CalibratedFieldStrength	1	PrivateTag
+(0019,"GEMS_ACQU_01",a4)	SS	SATFatWaterBone	1	PrivateTag
+(0019,"GEMS_ACQU_01",a5)	DS	ReceiveBandwidth	1	PrivateTag
+(0019,"GEMS_ACQU_01",a7)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",a8)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",a9)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",aa)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",ab)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",ac)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",ad)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",ae)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",af)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b0)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b1)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b2)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b3)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b4)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b5)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b6)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b7)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b8)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b9)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",ba)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",bb)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",bc)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",bd)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",be)	DS	ProjectionAngle	1	PrivateTag
+(0019,"GEMS_ACQU_01",c0)	SS	SaturationPlanes	1	PrivateTag
+(0019,"GEMS_ACQU_01",c1)	SS	SurfaceCoilIntensityCorrectionFlag	1	PrivateTag
+(0019,"GEMS_ACQU_01",c2)	SS	SATLocationR	1	PrivateTag
+(0019,"GEMS_ACQU_01",c3)	SS	SATLocationL	1	PrivateTag
+(0019,"GEMS_ACQU_01",c4)	SS	SATLocationA	1	PrivateTag
+(0019,"GEMS_ACQU_01",c5)	SS	SATLocationP	1	PrivateTag
+(0019,"GEMS_ACQU_01",c6)	SS	SATLocationH	1	PrivateTag
+(0019,"GEMS_ACQU_01",c7)	SS	SATLocationF	1	PrivateTag
+(0019,"GEMS_ACQU_01",c8)	SS	SATThicknessRL	1	PrivateTag
+(0019,"GEMS_ACQU_01",c9)	SS	SATThicknessAP	1	PrivateTag
+(0019,"GEMS_ACQU_01",ca)	SS	SATThicknessHF	1	PrivateTag
+(0019,"GEMS_ACQU_01",cb)	SS	PrescribedFlowAxis	1	PrivateTag
+(0019,"GEMS_ACQU_01",cc)	SS	VelocityEncoding	1	PrivateTag
+(0019,"GEMS_ACQU_01",cd)	SS	ThicknessDisclaimer	1	PrivateTag
+(0019,"GEMS_ACQU_01",ce)	SS	PrescanType	1	PrivateTag
+(0019,"GEMS_ACQU_01",cf)	SS	PrescanStatus	1	PrivateTag
+(0019,"GEMS_ACQU_01",d0)	SH	RawDataType	1	PrivateTag
+(0019,"GEMS_ACQU_01",d2)	SS	ProjectionAlgorithm	1	PrivateTag
+(0019,"GEMS_ACQU_01",d3)	SH	ProjectionAlgorithm	1	PrivateTag
+(0019,"GEMS_ACQU_01",d4)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",d5)	SS	FractionalEcho	1	PrivateTag
+(0019,"GEMS_ACQU_01",d6)	SS	PrepPulse	1	PrivateTag
+(0019,"GEMS_ACQU_01",d7)	SS	CardiacPhases	1	PrivateTag
+(0019,"GEMS_ACQU_01",d8)	SS	VariableEchoFlag	1	PrivateTag
+(0019,"GEMS_ACQU_01",d9)	DS	ConcatenatedSAT	1	PrivateTag
+(0019,"GEMS_ACQU_01",da)	SS	ReferenceChannelUsed	1	PrivateTag
+(0019,"GEMS_ACQU_01",db)	DS	BackProjectorCoefficient	1	PrivateTag
+(0019,"GEMS_ACQU_01",dc)	SS	PrimarySpeedCorrectionUsed	1	PrivateTag
+(0019,"GEMS_ACQU_01",dd)	SS	OverrangeCorrectionUsed	1	PrivateTag
+(0019,"GEMS_ACQU_01",de)	DS	DynamicZAlphaValue	1	PrivateTag
+(0019,"GEMS_ACQU_01",df)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",e0)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",e1)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",e2)	DS	VelocityEncodeScale	1	PrivateTag
+(0019,"GEMS_ACQU_01",e3)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",e4)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",e5)	IS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",e6)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",e8)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",e9)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",eb)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",ec)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",f0)	UN	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",f1)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",f2)	SS	FastPhases	1	PrivateTag
+(0019,"GEMS_ACQU_01",f3)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",f4)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",f9)	DS	TransmitGain	1	PrivateTag
+
+(0023,"GEMS_ACRQA_1.0 BLOCK1",00)	LO	CRExposureMenuCode	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",10)	LO	CRExposureMenuString	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",20)	LO	CREDRMode	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",30)	LO	CRLatitude	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",40)	LO	CRGroupNumber	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",50)	US	CRImageSerialNumber	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",60)	LO	CRBarCodeNumber	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",70)	LO	CRFilmOutputExposure	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",80)	LO	CRFilmFormat	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",90)	LO	CRSShiftString	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",00)	US	CRSShift	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",10)	DS	CRCShift	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",20)	DS	CRGT	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",30)	DS	CRGA	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",40)	DS	CRGC	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",50)	DS	CRGS	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",60)	DS	CRRT	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",70)	DS	CRRE	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",80)	US	CRRN	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",90)	DS	CRDRT	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",00)	DS	CRDRE	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",10)	US	CRDRN	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",20)	DS	CRORE	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",30)	US	CRORN	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",40)	US	CRORD	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",50)	LO	CRCassetteSize	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",60)	LO	CRMachineID	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",70)	LO	CRMachineType	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",80)	LO	CRTechnicianCode	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",90)	LO	CREnergySubtractionParameters	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",00)	LO	CRExposureMenuCode	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",10)	LO	CRExposureMenuString	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",20)	LO	CREDRMode	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",30)	LO	CRLatitude	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",40)	LO	CRGroupNumber	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",50)	US	CRImageSerialNumber	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",60)	LO	CRBarCodeNumber	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",70)	LO	CRFilmOutputExposure	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",80)	LO	CRFilmFormat	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",90)	LO	CRSShiftString	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",00)	US	CRSShift	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",10)	LO	CRCShift	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",20)	LO	CRGT	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",30)	DS	CRGA	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",40)	DS	CRGC	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",50)	DS	CRGS	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",60)	LO	CRRT	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",70)	DS	CRRE	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",80)	US	CRRN	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",90)	DS	CRDRT	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",00)	DS	CRDRE	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",10)	US	CRDRN	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",20)	DS	CRORE	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",30)	US	CRORN	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",40)	US	CRORD	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",50)	LO	CRCassetteSize	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",60)	LO	CRMachineID	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",70)	LO	CRMachineType	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",80)	LO	CRTechnicianCode	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",90)	LO	CREnergySubtractionParameters	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",f0)	LO	CRDistributionCode	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",ff)	US	CRShuttersApplied	1	PrivateTag
+
+(0047,"GEMS_ADWSoft_3D1",01)	SQ	Reconstruction Parameters Sequence	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",50)	UL	VolumeVoxelCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",51)	UL	VolumeSegmentCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",53)	US	VolumeSliceSize	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",54)	US	VolumeSliceCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",55)	SL	VolumeThresholdValue	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",57)	DS	VolumeVoxelRatio	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",58)	DS	VolumeVoxelSize	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",59)	US	VolumeZPositionSize	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",60)	DS	VolumeBaseLine	9	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",61)	DS	VolumeCenterPoint	3	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",63)	SL	VolumeSkewBase	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",64)	DS	VolumeRegistrationTransformRotationMatrix	9	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",65)	DS	VolumeRegistrationTransformTranslationVector	3	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",70)	DS	KVPList	1-n	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",71)	IS	XRayTubeCurrentList	1-n	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",72)	IS	ExposureList	1-n	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",80)	LO	AcquisitionDLXIdentifier	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",85)	SQ	AcquisitionDLX2DSeriesSequence	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",89)	DS	ContrastAgentVolumeList	1-n	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",8A)	US	NumberOfInjections	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",8B)	US	FrameCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",91)	LO	XA3DReconstructionAlgorithmName	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",92)	CS	XA3DReconstructionAlgorithmVersion	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",93)	DA	DLXCalibrationDate	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",94)	TM	DLXCalibrationTime	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",95)	CS	DLXCalibrationStatus	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",96)	IS	UsedFrames	1-n	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",98)	US	TransformCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",99)	SQ	TransformSequence	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",9A)	DS	TransformRotationMatrix	9	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",9B)	DS	TransformTranslationVector	3	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",9C)	LO	TransformLabel	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B0)	SQ	WireframeList	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B1)	US	WireframeCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B2)	US	LocationSystem	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B5)	LO	WireframeName	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B6)	LO	WireframeGroupName	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B7)	LO	WireframeColor	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B8)	SL	WireframeAttributes	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B9)	SL	WireframePointCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",BA)	SL	WireframeTimestamp	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",BB)	SQ	WireframePointList	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",BC)	DS	WireframePointsCoordinates	3	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",C0)	DS	VolumeUpperLeftHighCornerRAS	3	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",C1)	DS	VolumeSliceToRASRotationMatrix	9	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",C2)	DS	VolumeUpperLeftHighCornerTLOC	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",D1)	OB	VolumeSegmentList	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",D2)	OB	VolumeGradientList	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",D3)	OB	VolumeDensityList	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",D4)	OB	VolumeZPositionList	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",D5)	OB	VolumeOriginalIndexList	1	PrivateTag
+(0039,"GEMS_ADWSoft_DPO",80)	IS	PrivateEntityNumber	1	PrivateTag
+(0039,"GEMS_ADWSoft_DPO",85)	DA	PrivateEntityDate	1	PrivateTag
+(0039,"GEMS_ADWSoft_DPO",90)	TM	PrivateEntityTime	1	PrivateTag
+(0039,"GEMS_ADWSoft_DPO",95)	LO	PrivateEntityLaunchCommand	1	PrivateTag
+(0039,"GEMS_ADWSoft_DPO",AA)	CS	PrivateEntityType	1	PrivateTag
+
+(0033,"GEMS_CTHD_01",02)	UN	Unknown	1	PrivateTag
+
+(0037,"GEMS_DRS_1",10)	LO	ReferringDepartment	1	PrivateTag
+(0037,"GEMS_DRS_1",20)	US	ScreenNumber	1	PrivateTag
+(0037,"GEMS_DRS_1",40)	SH	LeftOrientation	1	PrivateTag
+(0037,"GEMS_DRS_1",42)	SH	RightOrientation	1	PrivateTag
+(0037,"GEMS_DRS_1",50)	CS	Inversion	1	PrivateTag
+(0037,"GEMS_DRS_1",60)	US	DSA	1	PrivateTag
+
+(0009,"GEMS_GENIE_1",10)	LO	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",11)	SL	StudyFlags	1	PrivateTag
+(0009,"GEMS_GENIE_1",12)	SL	StudyType	1	PrivateTag
+(0009,"GEMS_GENIE_1",1e)	UI	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",20)	LO	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",21)	SL	SeriesFlags	1	PrivateTag
+(0009,"GEMS_GENIE_1",22)	SH	UserOrientation	1	PrivateTag
+(0009,"GEMS_GENIE_1",23)	SL	InitiationType	1	PrivateTag
+(0009,"GEMS_GENIE_1",24)	SL	InitiationDelay	1	PrivateTag
+(0009,"GEMS_GENIE_1",25)	SL	InitiationCountRate	1	PrivateTag
+(0009,"GEMS_GENIE_1",26)	SL	NumberEnergySets	1	PrivateTag
+(0009,"GEMS_GENIE_1",27)	SL	NumberDetectors	1	PrivateTag
+(0009,"GEMS_GENIE_1",29)	SL	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",2a)	SL	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",2c)	LO	SeriesComments	1	PrivateTag
+(0009,"GEMS_GENIE_1",2d)	SL	TrackBeatAverage	1	PrivateTag
+(0009,"GEMS_GENIE_1",2e)	FD	DistancePrescribed	1	PrivateTag
+(0009,"GEMS_GENIE_1",30)	LO	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",35)	SL	GantryLocusType	1	PrivateTag
+(0009,"GEMS_GENIE_1",37)	SL	StartingHeartRate	1	PrivateTag
+(0009,"GEMS_GENIE_1",38)	SL	RRWindowWidth	1	PrivateTag
+(0009,"GEMS_GENIE_1",39)	SL	RRWindowOffset	1	PrivateTag
+(0009,"GEMS_GENIE_1",3a)	SL	PercentCycleImaged	1	PrivateTag
+(0009,"GEMS_GENIE_1",40)	LO	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",41)	SL	PatientFlags	1	PrivateTag
+(0009,"GEMS_GENIE_1",42)	DA	PatientCreationDate	1	PrivateTag
+(0009,"GEMS_GENIE_1",43)	TM	PatientCreationTime	1	PrivateTag
+(0011,"GEMS_GENIE_1",0a)	SL	SeriesType	1	PrivateTag
+(0011,"GEMS_GENIE_1",0b)	SL	EffectiveSeriesDuration	1	PrivateTag
+(0011,"GEMS_GENIE_1",0c)	SL	NumBeats	1	PrivateTag
+(0011,"GEMS_GENIE_1",0d)	LO	RadioNuclideName	1	PrivateTag
+(0011,"GEMS_GENIE_1",10)	LO	Unknown	1	PrivateTag
+(0011,"GEMS_GENIE_1",12)	LO	DatasetName	1	PrivateTag
+(0011,"GEMS_GENIE_1",13)	SL	DatasetType	1	PrivateTag
+(0011,"GEMS_GENIE_1",15)	SL	DetectorNumber	1	PrivateTag
+(0011,"GEMS_GENIE_1",16)	SL	EnergyNumber	1	PrivateTag
+(0011,"GEMS_GENIE_1",17)	SL	RRIntervalWindowNumber	1	PrivateTag
+(0011,"GEMS_GENIE_1",18)	SL	MGBinNumber	1	PrivateTag
+(0011,"GEMS_GENIE_1",19)	FD	RadiusOfRotation	1	PrivateTag
+(0011,"GEMS_GENIE_1",1a)	SL	DetectorCountZone	1	PrivateTag
+(0011,"GEMS_GENIE_1",1b)	SL	NumEnergyWindows	1	PrivateTag
+(0011,"GEMS_GENIE_1",1c)	SL	EnergyOffset	4	PrivateTag
+(0011,"GEMS_GENIE_1",1d)	SL	EnergyRange	1	PrivateTag
+(0011,"GEMS_GENIE_1",1f)	SL	ImageOrientation	1	PrivateTag
+(0011,"GEMS_GENIE_1",23)	SL	UseFOVMask	1	PrivateTag
+(0011,"GEMS_GENIE_1",24)	SL	FOVMaskYCutoffAngle	1	PrivateTag
+(0011,"GEMS_GENIE_1",25)	SL	FOVMaskCutoffAngle	1	PrivateTag
+(0011,"GEMS_GENIE_1",26)	SL	TableOrientation	1	PrivateTag
+(0011,"GEMS_GENIE_1",27)	SL	ROITopLeft	2	PrivateTag
+(0011,"GEMS_GENIE_1",28)	SL	ROIBottomRight	2	PrivateTag
+(0011,"GEMS_GENIE_1",30)	LO	Unknown	1	PrivateTag
+(0011,"GEMS_GENIE_1",33)	LO	EnergyCorrectName	1	PrivateTag
+(0011,"GEMS_GENIE_1",34)	LO	SpatialCorrectName	1	PrivateTag
+(0011,"GEMS_GENIE_1",35)	LO	TuningCalibName	1	PrivateTag
+(0011,"GEMS_GENIE_1",36)	LO	UniformityCorrectName	1	PrivateTag
+(0011,"GEMS_GENIE_1",37)	LO	AcquisitionSpecificCorrectName	1	PrivateTag
+(0011,"GEMS_GENIE_1",38)	SL	ByteOrder	1	PrivateTag
+(0011,"GEMS_GENIE_1",3a)	SL	PictureFormat	1	PrivateTag
+(0011,"GEMS_GENIE_1",3b)	FD	PixelScale	1	PrivateTag
+(0011,"GEMS_GENIE_1",3c)	FD	PixelOffset	1	PrivateTag
+(0011,"GEMS_GENIE_1",3e)	SL	FOVShape	1	PrivateTag
+(0011,"GEMS_GENIE_1",3f)	SL	DatasetFlags	1	PrivateTag
+(0011,"GEMS_GENIE_1",44)	FD	ThresholdCenter	1	PrivateTag
+(0011,"GEMS_GENIE_1",45)	FD	ThresholdWidth	1	PrivateTag
+(0011,"GEMS_GENIE_1",46)	SL	InterpolationType	1	PrivateTag
+(0011,"GEMS_GENIE_1",55)	FD	Period	1	PrivateTag
+(0011,"GEMS_GENIE_1",56)	FD	ElapsedTime	1	PrivateTag
+(0013,"GEMS_GENIE_1",10)	FD	DigitalFOV	2	PrivateTag
+(0013,"GEMS_GENIE_1",11)	SL	Unknown	1	PrivateTag
+(0013,"GEMS_GENIE_1",12)	SL	Unknown	1	PrivateTag
+(0013,"GEMS_GENIE_1",16)	SL	AutoTrackPeak	1	PrivateTag
+(0013,"GEMS_GENIE_1",17)	SL	AutoTrackWidth	1	PrivateTag
+(0013,"GEMS_GENIE_1",18)	FD	TransmissionScanTime	1	PrivateTag
+(0013,"GEMS_GENIE_1",19)	FD	TransmissionMaskWidth	1	PrivateTag
+(0013,"GEMS_GENIE_1",1a)	FD	CopperAttenuatorThickness	1	PrivateTag
+(0013,"GEMS_GENIE_1",1c)	FD	Unknown	1	PrivateTag
+(0013,"GEMS_GENIE_1",1d)	FD	Unknown	1	PrivateTag
+(0013,"GEMS_GENIE_1",1e)	FD	TomoViewOffset	1-n	PrivateTag
+(0013,"GEMS_GENIE_1",26)	LT	StudyComments	1	PrivateTag
+
+(0033,"GEMS_GNHD_01",01)	UN	Unknown	1	PrivateTag
+(0033,"GEMS_GNHD_01",02)	UN	Unknown	1	PrivateTag
+
+(0009,"GEMS_IDEN_01",01)	LO	FullFidelity	1	PrivateTag
+(0009,"GEMS_IDEN_01",02)	SH	SuiteId	1	PrivateTag
+(0009,"GEMS_IDEN_01",04)	SH	ProductId	1	PrivateTag
+(0009,"GEMS_IDEN_01",17)	LT	Unknown	1	PrivateTag
+(0009,"GEMS_IDEN_01",1a)	US	Unknown	1	PrivateTag
+(0009,"GEMS_IDEN_01",20)	US	Unknown	1	PrivateTag
+(0009,"GEMS_IDEN_01",27)	SL	ImageActualDate	1	PrivateTag
+(0009,"GEMS_IDEN_01",2f)	LT	Unknown	1	PrivateTag
+(0009,"GEMS_IDEN_01",30)	SH	ServiceId	1	PrivateTag
+(0009,"GEMS_IDEN_01",31)	SH	MobileLocationNumber	1	PrivateTag
+(0009,"GEMS_IDEN_01",e2)	LT	Unknown	1	PrivateTag
+(0009,"GEMS_IDEN_01",e3)	UI	EquipmentUID	1	PrivateTag
+(0009,"GEMS_IDEN_01",e6)	SH	GenesisVersionNow	1	PrivateTag
+(0009,"GEMS_IDEN_01",e7)	UL	ExamRecordChecksum	1	PrivateTag
+(0009,"GEMS_IDEN_01",e8)	UL	Unknown	1	PrivateTag
+(0009,"GEMS_IDEN_01",e9)	SL	ActualSeriesDataTimeStamp	1	PrivateTag
+
+(0027,"GEMS_IMAG_01",06)	SL	ImageArchiveFlag	1	PrivateTag
+(0027,"GEMS_IMAG_01",10)	SS	ScoutType	1	PrivateTag
+(0027,"GEMS_IMAG_01",1c)	SL	VmaMamp	1	PrivateTag
+(0027,"GEMS_IMAG_01",1d)	SS	VmaPhase	1	PrivateTag
+(0027,"GEMS_IMAG_01",1e)	SL	VmaMod	1	PrivateTag
+(0027,"GEMS_IMAG_01",1f)	SL	VmaClip	1	PrivateTag
+(0027,"GEMS_IMAG_01",20)	SS	SmartScanOnOffFlag	1	PrivateTag
+(0027,"GEMS_IMAG_01",30)	SH	ForeignImageRevision	1	PrivateTag
+(0027,"GEMS_IMAG_01",31)	SS	ImagingMode	1	PrivateTag
+(0027,"GEMS_IMAG_01",32)	SS	PulseSequence	1	PrivateTag
+(0027,"GEMS_IMAG_01",33)	SL	ImagingOptions	1	PrivateTag
+(0027,"GEMS_IMAG_01",35)	SS	PlaneType	1	PrivateTag
+(0027,"GEMS_IMAG_01",36)	SL	ObliquePlane	1	PrivateTag
+(0027,"GEMS_IMAG_01",40)	SH	RASLetterOfImageLocation	1	PrivateTag
+(0027,"GEMS_IMAG_01",41)	FL	ImageLocation	1	PrivateTag
+(0027,"GEMS_IMAG_01",42)	FL	CenterRCoordOfPlaneImage	1	PrivateTag
+(0027,"GEMS_IMAG_01",43)	FL	CenterACoordOfPlaneImage	1	PrivateTag
+(0027,"GEMS_IMAG_01",44)	FL	CenterSCoordOfPlaneImage	1	PrivateTag
+(0027,"GEMS_IMAG_01",45)	FL	NormalRCoord	1	PrivateTag
+(0027,"GEMS_IMAG_01",46)	FL	NormalACoord	1	PrivateTag
+(0027,"GEMS_IMAG_01",47)	FL	NormalSCoord	1	PrivateTag
+(0027,"GEMS_IMAG_01",48)	FL	RCoordOfTopRightCorner	1	PrivateTag
+(0027,"GEMS_IMAG_01",49)	FL	ACoordOfTopRightCorner	1	PrivateTag
+(0027,"GEMS_IMAG_01",4a)	FL	SCoordOfTopRightCorner	1	PrivateTag
+(0027,"GEMS_IMAG_01",4b)	FL	RCoordOfBottomRightCorner	1	PrivateTag
+(0027,"GEMS_IMAG_01",4c)	FL	ACoordOfBottomRightCorner	1	PrivateTag
+(0027,"GEMS_IMAG_01",4d)	FL	SCoordOfBottomRightCorner	1	PrivateTag
+(0027,"GEMS_IMAG_01",50)	FL	TableStartLocation	1	PrivateTag
+(0027,"GEMS_IMAG_01",51)	FL	TableEndLocation	1	PrivateTag
+(0027,"GEMS_IMAG_01",52)	SH	RASLetterForSideOfImage	1	PrivateTag
+(0027,"GEMS_IMAG_01",53)	SH	RASLetterForAnteriorPosterior	1	PrivateTag
+(0027,"GEMS_IMAG_01",54)	SH	RASLetterForScoutStartLoc	1	PrivateTag
+(0027,"GEMS_IMAG_01",55)	SH	RASLetterForScoutEndLoc	1	PrivateTag
+(0027,"GEMS_IMAG_01",60)	FL	ImageDimensionX	1	PrivateTag
+(0027,"GEMS_IMAG_01",61)	FL	ImageDimensionY	1	PrivateTag
+(0027,"GEMS_IMAG_01",62)	FL	NumberOfExcitations	1	PrivateTag
+
+(0029,"GEMS_IMPS_01",04)	SL	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",05)	DS	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",06)	DS	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",07)	SL	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",08)	SH	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",09)	SH	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",0a)	SS	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",15)	SL	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",16)	SL	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",17)	SL	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",18)	SL	UpperRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",1a)	SL	LengthOfTotalHeaderInBytes	1	PrivateTag
+(0029,"GEMS_IMPS_01",26)	SS	VersionOfHeaderStructure	1	PrivateTag
+(0029,"GEMS_IMPS_01",34)	SL	AdvantageCompOverflow	1	PrivateTag
+(0029,"GEMS_IMPS_01",35)	SL	AdvantageCompUnderflow	1	PrivateTag
+
+(0043,"GEMS_PARM_01",01)	SS	BitmapOfPrescanOptions	1	PrivateTag
+(0043,"GEMS_PARM_01",02)	SS	GradientOffsetInX	1	PrivateTag
+(0043,"GEMS_PARM_01",03)	SS	GradientOffsetInY	1	PrivateTag
+(0043,"GEMS_PARM_01",04)	SS	GradientOffsetInZ	1	PrivateTag
+(0043,"GEMS_PARM_01",05)	SS	ImageIsOriginalOrUnoriginal	1	PrivateTag
+(0043,"GEMS_PARM_01",06)	SS	NumberOfEPIShots	1	PrivateTag
+(0043,"GEMS_PARM_01",07)	SS	ViewsPerSegment	1	PrivateTag
+(0043,"GEMS_PARM_01",08)	SS	RespiratoryRateInBPM	1	PrivateTag
+(0043,"GEMS_PARM_01",09)	SS	RespiratoryTriggerPoint	1	PrivateTag
+(0043,"GEMS_PARM_01",0a)	SS	TypeOfReceiverUsed	1	PrivateTag
+(0043,"GEMS_PARM_01",0b)	DS	PeakRateOfChangeOfGradientField	1	PrivateTag
+(0043,"GEMS_PARM_01",0c)	DS	LimitsInUnitsOfPercent	1	PrivateTag
+(0043,"GEMS_PARM_01",0d)	DS	PSDEstimatedLimit	1	PrivateTag
+(0043,"GEMS_PARM_01",0e)	DS	PSDEstimatedLimitInTeslaPerSecond	1	PrivateTag
+(0043,"GEMS_PARM_01",0f)	DS	SARAvgHead	1	PrivateTag
+(0043,"GEMS_PARM_01",10)	US	WindowValue	1	PrivateTag
+(0043,"GEMS_PARM_01",11)	US	TotalInputViews	1	PrivateTag
+(0043,"GEMS_PARM_01",12)	SS	XrayChain	3	PrivateTag
+(0043,"GEMS_PARM_01",13)	SS	ReconKernelParameters	5	PrivateTag
+(0043,"GEMS_PARM_01",14)	SS	CalibrationParameters	3	PrivateTag
+(0043,"GEMS_PARM_01",15)	SS	TotalOutputViews	3	PrivateTag
+(0043,"GEMS_PARM_01",16)	SS	NumberOfOverranges	5	PrivateTag
+(0043,"GEMS_PARM_01",17)	DS	IBHImageScaleFactors	1	PrivateTag
+(0043,"GEMS_PARM_01",18)	DS	BBHCoefficients	3	PrivateTag
+(0043,"GEMS_PARM_01",19)	SS	NumberOfBBHChainsToBlend	1	PrivateTag
+(0043,"GEMS_PARM_01",1a)	SL	StartingChannelNumber	1	PrivateTag
+(0043,"GEMS_PARM_01",1b)	SS	PPScanParameters	1	PrivateTag
+(0043,"GEMS_PARM_01",1c)	SS	GEImageIntegrity	1	PrivateTag
+(0043,"GEMS_PARM_01",1d)	SS	LevelValue	1	PrivateTag
+(0043,"GEMS_PARM_01",1e)	DS	DeltaStartTime	1	PrivateTag
+(0043,"GEMS_PARM_01",1f)	SL	MaxOverrangesInAView	1	PrivateTag
+(0043,"GEMS_PARM_01",20)	DS	AvgOverrangesAllViews	1	PrivateTag
+(0043,"GEMS_PARM_01",21)	SS	CorrectedAfterglowTerms	1	PrivateTag
+(0043,"GEMS_PARM_01",25)	SS	ReferenceChannels	6	PrivateTag
+(0043,"GEMS_PARM_01",26)	US	NoViewsRefChannelsBlocked	6	PrivateTag
+(0043,"GEMS_PARM_01",27)	SH	ScanPitchRatio	1	PrivateTag
+(0043,"GEMS_PARM_01",28)	OB	UniqueImageIdentifier	1	PrivateTag
+(0043,"GEMS_PARM_01",29)	OB	HistogramTables	1	PrivateTag
+(0043,"GEMS_PARM_01",2a)	OB	UserDefinedData	1	PrivateTag
+(0043,"GEMS_PARM_01",2b)	SS	PrivateScanOptions	4	PrivateTag
+(0043,"GEMS_PARM_01",2c)	SS	EffectiveEchoSpacing	1	PrivateTag
+(0043,"GEMS_PARM_01",2d)	SH	StringSlopField1	1	PrivateTag
+(0043,"GEMS_PARM_01",2e)	SH	StringSlopField2	1	PrivateTag
+(0043,"GEMS_PARM_01",2f)	SS	RawDataType	1	PrivateTag
+(0043,"GEMS_PARM_01",30)	SS	RawDataType	1	PrivateTag
+(0043,"GEMS_PARM_01",31)	DS	RACoordOfTargetReconCentre	2	PrivateTag
+(0043,"GEMS_PARM_01",32)	SS	RawDataType	1	PrivateTag
+(0043,"GEMS_PARM_01",33)	FL	NegScanSpacing	1	PrivateTag
+(0043,"GEMS_PARM_01",34)	IS	OffsetFrequency	1	PrivateTag
+(0043,"GEMS_PARM_01",35)	UL	UserUsageTag	1	PrivateTag
+(0043,"GEMS_PARM_01",36)	UL	UserFillMapMSW	1	PrivateTag
+(0043,"GEMS_PARM_01",37)	UL	UserFillMapLSW	1	PrivateTag
+(0043,"GEMS_PARM_01",38)	FL	User25ToUser48	24	PrivateTag
+(0043,"GEMS_PARM_01",39)	IS	SlopInteger6ToSlopInteger9	4	PrivateTag
+(0043,"GEMS_PARM_01",40)	FL	TriggerOnPosition	4	PrivateTag
+(0043,"GEMS_PARM_01",41)	FL	DegreeOfRotation	4	PrivateTag
+(0043,"GEMS_PARM_01",42)	SL	DASTriggerSource	4	PrivateTag
+(0043,"GEMS_PARM_01",43)	SL	DASFpaGain	4	PrivateTag
+(0043,"GEMS_PARM_01",44)	SL	DASOutputSource	4	PrivateTag
+(0043,"GEMS_PARM_01",45)	SL	DASAdInput	4	PrivateTag
+(0043,"GEMS_PARM_01",46)	SL	DASCalMode	4	PrivateTag
+(0043,"GEMS_PARM_01",47)	SL	DASCalFrequency	4	PrivateTag
+(0043,"GEMS_PARM_01",48)	SL	DASRegXm	4	PrivateTag
+(0043,"GEMS_PARM_01",49)	SL	DASAutoZero	4	PrivateTag
+(0043,"GEMS_PARM_01",4a)	SS	StartingChannelOfView	4	PrivateTag
+(0043,"GEMS_PARM_01",4b)	SL	DASXmPattern	4	PrivateTag
+(0043,"GEMS_PARM_01",4c)	SS	TGGCTriggerMode	4	PrivateTag
+(0043,"GEMS_PARM_01",4d)	FL	StartScanToXrayOnDelay	4	PrivateTag
+(0043,"GEMS_PARM_01",4e)	FL	DurationOfXrayOn	4	PrivateTag
+(0043,"GEMS_PARM_01",60)	IS	SlopInteger10ToSlopInteger17	8	PrivateTag
+(0043,"GEMS_PARM_01",61)	UI	ScannerStudyEntityUID	1	PrivateTag
+(0043,"GEMS_PARM_01",62)	SH	ScannerStudyID	1	PrivateTag
+(0043,"GEMS_PARM_01",6f)	DS	ScannerTableEntry	3	PrivateTag
+(0043,"GEMS_PARM_01",70)	LO	ParadigmName	1	PrivateTag
+(0043,"GEMS_PARM_01",71)	ST	ParadigmDescription	1	PrivateTag
+(0043,"GEMS_PARM_01",72)	UI	ParadigmUID	1	PrivateTag
+(0043,"GEMS_PARM_01",73)	US	ExperimentType	1	PrivateTag
+(0043,"GEMS_PARM_01",74)	US	NumberOfRestVolumes	1	PrivateTag
+(0043,"GEMS_PARM_01",75)	US	NumberOfActiveVolumes	1	PrivateTag
+(0043,"GEMS_PARM_01",76)	US	NumberOfDummyScans	1	PrivateTag
+(0043,"GEMS_PARM_01",77)	SH	ApplicationName	1	PrivateTag
+(0043,"GEMS_PARM_01",78)	SH	ApplicationVersion	1	PrivateTag
+(0043,"GEMS_PARM_01",79)	US	SlicesPerVolume	1	PrivateTag
+(0043,"GEMS_PARM_01",7a)	US	ExpectedTimePoints	1	PrivateTag
+(0043,"GEMS_PARM_01",7b)	FL	RegressorValues	1-n	PrivateTag
+(0043,"GEMS_PARM_01",7c)	FL	DelayAfterSliceGroup	1	PrivateTag
+(0043,"GEMS_PARM_01",7d)	US	ReconModeFlagWord	1	PrivateTag
+(0043,"GEMS_PARM_01",7e)	LO	PACCSpecificInformation	1-n	PrivateTag
+(0043,"GEMS_PARM_01",7f)	DS	EDWIScaleFactor	1-n	PrivateTag
+(0043,"GEMS_PARM_01",80)	LO	CoilIDData	1-n	PrivateTag
+(0043,"GEMS_PARM_01",81)	LO	GECoilName	1	PrivateTag
+(0043,"GEMS_PARM_01",82)	LO	SystemConfigurationInformation	1-n	PrivateTag
+(0043,"GEMS_PARM_01",83)	DS	AssetRFactors	1-2	PrivateTag
+(0043,"GEMS_PARM_01",84)	LO	AdditionalAssetData	5-n	PrivateTag
+(0043,"GEMS_PARM_01",85)	UT	DebugDataTextFormat	1	PrivateTag
+(0043,"GEMS_PARM_01",86)	OB	DebugDataBinaryFormat	1	PrivateTag
+(0043,"GEMS_PARM_01",87)	UT	ScannerSoftwareVersionLongForm	1	PrivateTag
+(0043,"GEMS_PARM_01",88)	UI	PUREAcquisitionCalibrationSeriesUID	1	PrivateTag
+(0043,"GEMS_PARM_01",89)	LO	GoverningBodydBdtAndSARDefinition	3	PrivateTag
+(0043,"GEMS_PARM_01",8a)	CS	PrivateInPlanePhaseEncodingDirection	1	PrivateTag
+(0043,"GEMS_PARM_01",8b)	OB	FMRIBinaryDataBlock	1	PrivateTag
+(0043,"GEMS_PARM_01",8c)	DS	VoxelLocation	6	PrivateTag
+(0043,"GEMS_PARM_01",8d)	DS	SATBandLocations	7-7n	PrivateTag
+(0043,"GEMS_PARM_01",8e)	DS	SpectroPrescanValues	3	PrivateTag
+(0043,"GEMS_PARM_01",8f)	DS	SpectroParameters	3	PrivateTag
+(0043,"GEMS_PARM_01",90)	LO	SARDefinition	1-n	PrivateTag
+(0043,"GEMS_PARM_01",91)	DS	SARValue	1-n	PrivateTag
+(0043,"GEMS_PARM_01",92)	LO	ImageErrorText	1	PrivateTag
+(0043,"GEMS_PARM_01",93)	DS	SpectroQuantitationValues	1-n	PrivateTag
+(0043,"GEMS_PARM_01",94)	DS	SpectroRatioValues	1-n	PrivateTag
+(0043,"GEMS_PARM_01",95)	LO	PrescanReuseString	1	PrivateTag
+(0043,"GEMS_PARM_01",96)	CS	ContentQualification	1	PrivateTag
+(0043,"GEMS_PARM_01",97)	LO	ImageFilteringParameters	9	PrivateTag
+(0043,"GEMS_PARM_01",98)	UI	ASSETAcquisitionCalibrationSeriesUID	1	PrivateTag
+(0043,"GEMS_PARM_01",99)	LO	ExtendedOptions	1-n	PrivateTag
+(0043,"GEMS_PARM_01",9a)	IS	RxStackIdentification	1	PrivateTag
+(0043,"GEMS_PARM_01",9b)	DS	NPWFactor	1	PrivateTag
+(0043,"GEMS_PARM_01",9c)	OB	ResearchTag1	1	PrivateTag
+(0043,"GEMS_PARM_01",9d)	OB	ResearchTag2	1	PrivateTag
+(0043,"GEMS_PARM_01",9e)	OB	ResearchTag3	1	PrivateTag
+(0043,"GEMS_PARM_01",9f)	OB	ResearchTag4	1	PrivateTag
+
+(0011,"GEMS_PATI_01",10)	SS	PatientStatus	1	PrivateTag
+
+(0021,"GEMS_RELA_01",03)	SS	SeriesFromWhichPrescribed	1	PrivateTag
+(0021,"GEMS_RELA_01",05)	SH	GenesisVersionNow	1	PrivateTag
+(0021,"GEMS_RELA_01",07)	UL	SeriesRecordChecksum	1	PrivateTag
+(0021,"GEMS_RELA_01",15)	US	Unknown	1	PrivateTag
+(0021,"GEMS_RELA_01",16)	SS	Unknown	1	PrivateTag
+(0021,"GEMS_RELA_01",18)	SH	GenesisVersionNow	1	PrivateTag
+(0021,"GEMS_RELA_01",19)	UL	AcqReconRecordChecksum	1	PrivateTag
+(0021,"GEMS_RELA_01",20)	DS	TableStartLocation	1	PrivateTag
+(0021,"GEMS_RELA_01",35)	SS	SeriesFromWhichPrescribed	1	PrivateTag
+(0021,"GEMS_RELA_01",36)	SS	ImageFromWhichPrescribed	1	PrivateTag
+(0021,"GEMS_RELA_01",37)	SS	ScreenFormat	1	PrivateTag
+(0021,"GEMS_RELA_01",4a)	LO	AnatomicalReferenceForScout	1	PrivateTag
+(0021,"GEMS_RELA_01",4e)	US	Unknown	1	PrivateTag
+(0021,"GEMS_RELA_01",4f)	SS	LocationsInAcquisition	1	PrivateTag
+(0021,"GEMS_RELA_01",50)	SS	GraphicallyPrescribed	1	PrivateTag
+(0021,"GEMS_RELA_01",51)	DS	RotationFromSourceXRot	1	PrivateTag
+(0021,"GEMS_RELA_01",52)	DS	RotationFromSourceYRot	1	PrivateTag
+(0021,"GEMS_RELA_01",53)	DS	RotationFromSourceZRot	1	PrivateTag
+(0021,"GEMS_RELA_01",54)	SH	ImagePosition	3	PrivateTag
+(0021,"GEMS_RELA_01",55)	SH	ImageOrientation	6	PrivateTag
+(0021,"GEMS_RELA_01",56)	SL	IntegerSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",57)	SL	IntegerSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",58)	SL	IntegerSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",59)	SL	IntegerSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",5a)	SL	IntegerSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",5b)	DS	FloatSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",5c)	DS	FloatSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",5d)	DS	FloatSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",5e)	DS	FloatSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",5f)	DS	FloatSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",70)	LT	Unknown	1	PrivateTag
+(0021,"GEMS_RELA_01",71)	LT	Unknown	1	PrivateTag
+(0021,"GEMS_RELA_01",81)	DS	AutoWindowLevelAlpha	1	PrivateTag
+(0021,"GEMS_RELA_01",82)	DS	AutoWindowLevelBeta	1	PrivateTag
+(0021,"GEMS_RELA_01",83)	DS	AutoWindowLevelWindow	1	PrivateTag
+(0021,"GEMS_RELA_01",84)	DS	AutoWindowLevelLevel	1	PrivateTag
+(0021,"GEMS_RELA_01",90)	SS	TubeFocalSpotPosition	1	PrivateTag
+(0021,"GEMS_RELA_01",91)	SS	BiopsyPosition	1	PrivateTag
+(0021,"GEMS_RELA_01",92)	FL	BiopsyTLocation	1	PrivateTag
+(0021,"GEMS_RELA_01",93)	FL	BiopsyRefLocation	1	PrivateTag
+
+(0045,"GEMS_SENO_02",04)	CS	AES	1	PrivateTag
+(0045,"GEMS_SENO_02",06)	DS	Angulation	1	PrivateTag
+(0045,"GEMS_SENO_02",09)	DS	RealMagnificationFactor	1	PrivateTag
+(0045,"GEMS_SENO_02",0b)	CS	SenographType	1	PrivateTag
+(0045,"GEMS_SENO_02",0c)	DS	IntegrationTime	1	PrivateTag
+(0045,"GEMS_SENO_02",0d)	DS	ROIOriginXY	1	PrivateTag
+(0045,"GEMS_SENO_02",11)	DS	ReceptorSizeCmXY	2	PrivateTag
+(0045,"GEMS_SENO_02",12)	IS	ReceptorSizePixelsXY	2	PrivateTag
+(0045,"GEMS_SENO_02",13)	ST	Screen	1	PrivateTag
+(0045,"GEMS_SENO_02",14)	DS	PixelPitchMicrons	1	PrivateTag
+(0045,"GEMS_SENO_02",15)	IS	PixelDepthBits	1	PrivateTag
+(0045,"GEMS_SENO_02",16)	IS	BinningFactorXY	2	PrivateTag
+(0045,"GEMS_SENO_02",1B)	CS	ClinicalView	1	PrivateTag
+(0045,"GEMS_SENO_02",1D)	DS	MeanOfRawGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",1E)	DS	MeanOfOffsetGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",1F)	DS	MeanOfCorrectedGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",20)	DS	MeanOfRegionGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",21)	DS	MeanOfLogRegionGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",22)	DS	StandardDeviationOfRawGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",23)	DS	StandardDeviationOfCorrectedGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",24)	DS	StandardDeviationOfRegionGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",25)	DS	StandardDeviationOfLogRegionGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",26)	OB	MAOBuffer	1	PrivateTag
+(0045,"GEMS_SENO_02",27)	IS	SetNumber	1	PrivateTag
+(0045,"GEMS_SENO_02",28)	CS	WindowingType	1	PrivateTag
+(0045,"GEMS_SENO_02",29)	DS	WindowingParameters	1-n	PrivateTag
+(0045,"GEMS_SENO_02",2a)	IS	CrosshairCursorXCoordinates	1	PrivateTag
+(0045,"GEMS_SENO_02",2b)	IS	CrosshairCursorYCoordinates	1	PrivateTag
+(0045,"GEMS_SENO_02",39)	US	VignetteRows	1	PrivateTag
+(0045,"GEMS_SENO_02",3a)	US	VignetteColumns	1	PrivateTag
+(0045,"GEMS_SENO_02",3b)	US	VignetteBitsAllocated	1	PrivateTag
+(0045,"GEMS_SENO_02",3c)	US	VignetteBitsStored	1	PrivateTag
+(0045,"GEMS_SENO_02",3d)	US	VignetteHighBit	1	PrivateTag
+(0045,"GEMS_SENO_02",3e)	US	VignettePixelRepresentation	1	PrivateTag
+(0045,"GEMS_SENO_02",3f)	OB	VignettePixelData	1	PrivateTag
+
+(0025,"GEMS_SERS_01",06)	SS	LastPulseSequenceUsed	1	PrivateTag
+(0025,"GEMS_SERS_01",07)	SL	ImagesInSeries	1	PrivateTag
+(0025,"GEMS_SERS_01",10)	SL	LandmarkCounter	1	PrivateTag
+(0025,"GEMS_SERS_01",11)	SS	NumberOfAcquisitions	1	PrivateTag
+(0025,"GEMS_SERS_01",14)	SL	IndicatesNumberOfUpdatesToHeader	1	PrivateTag
+(0025,"GEMS_SERS_01",17)	SL	SeriesCompleteFlag	1	PrivateTag
+(0025,"GEMS_SERS_01",18)	SL	NumberOfImagesArchived	1	PrivateTag
+(0025,"GEMS_SERS_01",19)	SL	LastImageNumberUsed	1	PrivateTag
+(0025,"GEMS_SERS_01",1a)	SH	PrimaryReceiverSuiteAndHost	1	PrivateTag
+
+(0023,"GEMS_STDY_01",01)	SL	NumberOfSeriesInStudy	1	PrivateTag
+(0023,"GEMS_STDY_01",02)	SL	NumberOfUnarchivedSeries	1	PrivateTag
+(0023,"GEMS_STDY_01",10)	SS	ReferenceImageField	1	PrivateTag
+(0023,"GEMS_STDY_01",50)	SS	SummaryImage	1	PrivateTag
+(0023,"GEMS_STDY_01",70)	FD	StartTimeSecsInFirstAxial	1	PrivateTag
+(0023,"GEMS_STDY_01",74)	SL	NumberOfUpdatesToHeader	1	PrivateTag
+(0023,"GEMS_STDY_01",7d)	SS	IndicatesIfStudyHasCompleteInfo	1	PrivateTag
+
+(0033,"GEMS_YMHD_01",05)	UN	Unknown	1	PrivateTag
+(0033,"GEMS_YMHD_01",06)	UN	Unknown	1	PrivateTag
+
+(0019,"GE_GENESIS_REV3.0",39)	SS	AxialType	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",8f)	SS	SwapPhaseFrequency	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",9c)	SS	PulseSequenceName	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",9f)	SS	CoilType	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",a4)	SS	SATFatWaterBone	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",c0)	SS	BitmapOfSATSelections	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",c1)	SS	SurfaceCoilIntensityCorrectionFlag	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",cb)	SS	PhaseContrastFlowAxis	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",cc)	SS	PhaseContrastVelocityEncoding	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",d5)	SS	FractionalEcho	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",d8)	SS	VariableEchoFlag	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",d9)	DS	ConcatenatedSat	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",f2)	SS	NumberOfPhases	1	PrivateTag
+(0043,"GE_GENESIS_REV3.0",1e)	DS	DeltaStartTime	1	PrivateTag
+(0043,"GE_GENESIS_REV3.0",27)	SH	ScanPitchRatio	1	PrivateTag
+
+(0029,"INTELERAD MEDICAL SYSTEMS",01)	FD	ImageCompressionFraction	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",02)	FD	ImageQuality	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",03)	FD	ImageBytesTransferred	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",10)	SH	J2cParameterType	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",11)	US	J2cPixelRepresentation	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",12)	US	J2cBitsAllocated	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",13)	US	J2cPixelShiftValue	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",14)	US	J2cPlanarConfiguration	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",15)	DS	J2cRescaleIntercept	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",20)	LO	PixelDataMD5SumPerFrame	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",21)	US	HistogramPercentileLabels	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",22)	FD	HistogramPercentileValues	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",01)	LO	InstitutionCode	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",02)	LO	RoutedTransferAE	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",03)	LO	SourceAE	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",04)	SH	DeferredValidation	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",05)	LO	SeriesOwner	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",06)	LO	OrderGroupNumber	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",07)	SH	StrippedPixelData	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",08)	SH	PendingMoveRequest	1	PrivateTag
+
+(0041,"INTEGRIS 1.0",20)	FL	AccumulatedFluoroscopyDose	1	PrivateTag
+(0041,"INTEGRIS 1.0",30)	FL	AccumulatedExposureDose	1	PrivateTag
+(0041,"INTEGRIS 1.0",40)	FL	TotalDose	1	PrivateTag
+(0041,"INTEGRIS 1.0",41)	FL	TotalNumberOfFrames	1	PrivateTag
+(0041,"INTEGRIS 1.0",50)	SQ	ExposureInformationSequence	1	PrivateTag
+(0009,"INTEGRIS 1.0",08)	CS	ExposureChannel	1-n	PrivateTag
+(0009,"INTEGRIS 1.0",32)	TM	ExposureStartTime	1	PrivateTag
+(0019,"INTEGRIS 1.0",00)	LO	APRName	1	PrivateTag
+(0019,"INTEGRIS 1.0",40)	DS	FrameRate	1	PrivateTag
+(0021,"INTEGRIS 1.0",12)	IS	ExposureNumber	1	PrivateTag
+(0029,"INTEGRIS 1.0",08)	IS	NumberOfExposureResults	1	PrivateTag
+
+(0029,"ISG shadow",70)	IS	Unknown	1	PrivateTag
+(0029,"ISG shadow",80)	IS	Unknown	1	PrivateTag
+(0029,"ISG shadow",90)	IS	Unknown	1	PrivateTag
+
+(0009,"ISI",01)	UN	SIENETGeneralPurposeIMGEF	1	PrivateTag
+
+(0009,"MERGE TECHNOLOGIES, INC.",00)	OB	Unknown	1	PrivateTag
+
+(0029,"OCULUS Optikgeraete GmbH",1010)	OB	OriginalMeasuringData	1	PrivateTag
+(0029,"OCULUS Optikgeraete GmbH",1012)	UL	OriginalMeasuringDataLength	1	PrivateTag
+(0029,"OCULUS Optikgeraete GmbH",1020)	OB	OriginalMeasuringRawData	1	PrivateTag
+(0029,"OCULUS Optikgeraete GmbH",1022)	UL	OriginalMeasuringRawDataLength	1	PrivateTag
+
+(0041,"PAPYRUS 3.0",00)	LT	PapyrusComments	1	PrivateTag
+(0041,"PAPYRUS 3.0",10)	SQ	PointerSequence	1	PrivateTag
+(0041,"PAPYRUS 3.0",11)	UL	ImagePointer	1	PrivateTag
+(0041,"PAPYRUS 3.0",12)	UL	PixelOffset	1	PrivateTag
+(0041,"PAPYRUS 3.0",13)	SQ	ImageIdentifierSequence	1	PrivateTag
+(0041,"PAPYRUS 3.0",14)	SQ	ExternalFileReferenceSequence	1	PrivateTag
+(0041,"PAPYRUS 3.0",15)	US	NumberOfImages	1	PrivateTag
+(0041,"PAPYRUS 3.0",21)	UI	ReferencedSOPClassUID	1	PrivateTag
+(0041,"PAPYRUS 3.0",22)	UI	ReferencedSOPInstanceUID	1	PrivateTag
+(0041,"PAPYRUS 3.0",31)	LT	ReferencedFileName	1	PrivateTag
+(0041,"PAPYRUS 3.0",32)	LT	ReferencedFilePath	1-n	PrivateTag
+(0041,"PAPYRUS 3.0",41)	UI	ReferencedImageSOPClassUID	1	PrivateTag
+(0041,"PAPYRUS 3.0",42)	UI	ReferencedImageSOPInstanceUID	1	PrivateTag
+(0041,"PAPYRUS 3.0",50)	SQ	ImageSequence	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",00)	IS	OverlayID	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",01)	LT	LinkedOverlays	1-n	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",10)	US	OverlayRows	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",11)	US	OverlayColumns	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",40)	LO	OverlayType	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",50)	US	OverlayOrigin	1-n	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",60)	LO	Editable	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",70)	LO	OverlayFont	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",72)	LO	OverlayStyle	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",74)	US	OverlayFontSize	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",76)	LO	OverlayColor	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",78)	US	ShadowSize	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",80)	LO	FillPattern	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",82)	US	OverlayPenSize	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",a0)	LO	Label	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",a2)	LT	PostItText	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",a4)	US	AnchorPoint	2	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",b0)	LO	ROIType	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",b2)	LT	AttachedAnnotation	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",ba)	US	ContourPoints	1-n	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",bc)	US	MaskData	1-n	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",c0)	SQ	UINOverlaySequence	1	PrivateTag
+
+(0009,"PAPYRUS",00)	LT	OriginalFileName	1	PrivateTag
+(0009,"PAPYRUS",10)	LT	OriginalFileLocation	1	PrivateTag
+(0009,"PAPYRUS",18)	LT	DataSetIdentifier	1	PrivateTag
+(0041,"PAPYRUS",00)	LT	PapyrusComments	1-n	PrivateTag
+(0041,"PAPYRUS",10)	US	FolderType	1	PrivateTag
+(0041,"PAPYRUS",11)	LT	PatientFolderDataSetID	1	PrivateTag
+(0041,"PAPYRUS",20)	LT	FolderName	1	PrivateTag
+(0041,"PAPYRUS",30)	DA	CreationDate	1	PrivateTag
+(0041,"PAPYRUS",32)	TM	CreationTime	1	PrivateTag
+(0041,"PAPYRUS",34)	DA	ModifiedDate	1	PrivateTag
+(0041,"PAPYRUS",36)	TM	ModifiedTime	1	PrivateTag
+(0041,"PAPYRUS",40)	LT	OwnerName	1-n	PrivateTag
+(0041,"PAPYRUS",50)	LT	FolderStatus	1	PrivateTag
+(0041,"PAPYRUS",60)	UL	NumberOfImages	1	PrivateTag
+(0041,"PAPYRUS",62)	UL	NumberOfOther	1	PrivateTag
+(0041,"PAPYRUS",a0)	LT	ExternalFolderElementDSID	1-n	PrivateTag
+(0041,"PAPYRUS",a1)	US	ExternalFolderElementDataSetType	1-n	PrivateTag
+(0041,"PAPYRUS",a2)	LT	ExternalFolderElementFileLocation	1-n	PrivateTag
+(0041,"PAPYRUS",a3)	UL	ExternalFolderElementLength	1-n	PrivateTag
+(0041,"PAPYRUS",b0)	LT	InternalFolderElementDSID	1-n	PrivateTag
+(0041,"PAPYRUS",b1)	US	InternalFolderElementDataSetType	1-n	PrivateTag
+(0041,"PAPYRUS",b2)	UL	InternalOffsetToDataSet	1-n	PrivateTag
+(0041,"PAPYRUS",b3)	UL	InternalOffsetToImage	1-n
+
+# Note: Some Philips devices use these private tags with reservation value
+# "Philips Imaging DD 001", others use "PHILIPS IMAGING DD 001". All attributes
+# should thus be present twice in this dictionary, once for each spelling variant.
+#
+(2001,"Philips Imaging DD 001",01)	FL	ChemicalShift	1	PrivateTag
+(2001,"Philips Imaging DD 001",02)	IS	ChemicalShiftNumberMR	1	PrivateTag
+(2001,"Philips Imaging DD 001",03)	FL	DiffusionBFactor	1	PrivateTag
+(2001,"Philips Imaging DD 001",04)	CS	DiffusionDirection	1	PrivateTag
+(2001,"Philips Imaging DD 001",06)	CS	ImageEnhanced	1	PrivateTag
+(2001,"Philips Imaging DD 001",07)	CS	ImageTypeEDES	1	PrivateTag
+(2001,"Philips Imaging DD 001",08)	IS	PhaseNumber	1	PrivateTag
+(2001,"Philips Imaging DD 001",09)	FL	ImagePrepulseDelay	1	PrivateTag
+(2001,"Philips Imaging DD 001",0a)	IS	SliceNumberMR	1	PrivateTag
+(2001,"Philips Imaging DD 001",0b)	CS	SliceOrientation	1	PrivateTag
+(2001,"Philips Imaging DD 001",0c)	CS	ArrhythmiaRejection	1	PrivateTag
+(2001,"Philips Imaging DD 001",0e)	CS	CardiacCycled	1	PrivateTag
+(2001,"Philips Imaging DD 001",0f)	SS	CardiacGateWidth	1	PrivateTag
+(2001,"Philips Imaging DD 001",10)	CS	CardiacSync	1	PrivateTag
+(2001,"Philips Imaging DD 001",11)	FL	DiffusionEchoTime	1	PrivateTag
+(2001,"Philips Imaging DD 001",12)	CS	DynamicSeries	1	PrivateTag
+(2001,"Philips Imaging DD 001",13)	SL	EPIFactor	1	PrivateTag
+(2001,"Philips Imaging DD 001",14)	SL	NumberOfEchoes	1	PrivateTag
+(2001,"Philips Imaging DD 001",15)	SS	NumberOfLocations	1	PrivateTag
+(2001,"Philips Imaging DD 001",16)	SS	NumberOfPCDirections	1	PrivateTag
+(2001,"Philips Imaging DD 001",17)	SL	NumberOfPhasesMR	1	PrivateTag
+(2001,"Philips Imaging DD 001",18)	SL	NumberOfSlicesMR	1	PrivateTag
+(2001,"Philips Imaging DD 001",19)	CS	PartialMatrixScanned	1	PrivateTag
+(2001,"Philips Imaging DD 001",1a)	FL	PCVelocity	1-n	PrivateTag
+(2001,"Philips Imaging DD 001",1b)	FL	PrepulseDelay	1	PrivateTag
+(2001,"Philips Imaging DD 001",1c)	CS	PrepulseType	1	PrivateTag
+(2001,"Philips Imaging DD 001",1d)	IS	ReconstructionNumberMR	1	PrivateTag
+(2001,"Philips Imaging DD 001",1f)	CS	RespirationSync	1	PrivateTag
+(2001,"Philips Imaging DD 001",20)	LO	ScanningTechnique	1	PrivateTag
+(2001,"Philips Imaging DD 001",21)	CS	SPIR	1	PrivateTag
+(2001,"Philips Imaging DD 001",22)	FL	WaterFatShift	1	PrivateTag
+(2001,"Philips Imaging DD 001",23)	DS	FlipAnglePhilips	1	PrivateTag
+(2001,"Philips Imaging DD 001",24)	CS	SeriesIsInteractive	1	PrivateTag
+(2001,"Philips Imaging DD 001",25)	SH	EchoTimeDisplayMR	1	PrivateTag
+(2001,"Philips Imaging DD 001",26)	CS	PresentationStateSubtractionActive	1	PrivateTag
+(2001,"Philips Imaging DD 001",2d)	SS	StackNumberOfSlices	1	PrivateTag
+(2001,"Philips Imaging DD 001",32)	FL	StackRadialAngle	1	PrivateTag
+(2001,"Philips Imaging DD 001",33)	CS	StackRadialAxis	1	PrivateTag
+(2001,"Philips Imaging DD 001",35)	SS	StackSliceNumber	1	PrivateTag
+(2001,"Philips Imaging DD 001",36)	CS	StackType	1	PrivateTag
+(2001,"Philips Imaging DD 001",3f)	CS	ZoomMode	1	PrivateTag
+(2001,"Philips Imaging DD 001",58)	UL	ContrastTransferTaste	1	PrivateTag
+(2001,"Philips Imaging DD 001",5f)	SQ	StackSequence	1	PrivateTag
+(2001,"Philips Imaging DD 001",60)	SL	NumberOfStacks	1	PrivateTag
+(2001,"Philips Imaging DD 001",61)	CS	SeriesTransmitted	1	PrivateTag
+(2001,"Philips Imaging DD 001",62)	CS	SeriesCommitted	1	PrivateTag
+(2001,"Philips Imaging DD 001",63)	CS	ExaminationSource	1	PrivateTag
+(2001,"Philips Imaging DD 001",67)	CS	LinearPresentationGLTrafoShapeSub	1	PrivateTag
+(2001,"Philips Imaging DD 001",77)	CS	GLTrafoType	1	PrivateTag
+(2001,"Philips Imaging DD 001",7b)	IS	AcquisitionNumber	1	PrivateTag
+(2001,"Philips Imaging DD 001",81)	IS	NumberOfDynamicScans	1	PrivateTag
+(2001,"Philips Imaging DD 001",9f)	US	PixelProcessingKernelSize	1	PrivateTag
+(2001,"Philips Imaging DD 001",a1)	CS	IsRawImage	1	PrivateTag
+(2001,"Philips Imaging DD 001",f1)	FL	ProspectiveMotionCorrection	1	PrivateTag
+(2001,"Philips Imaging DD 001",f2)	FL	RetrospectiveMotionCorrection	1	PrivateTag
+
+# Note: Some Philips devices use these private tags with reservation value
+# "Philips Imaging DD 001", others use "PHILIPS IMAGING DD 001". All attributes
+# should thus be present twice in this dictionary, once for each spelling variant.
+#
+(2001,"PHILIPS IMAGING DD 001",01)	FL	ChemicalShift	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",02)	IS	ChemicalShiftNumberMR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",03)	FL	DiffusionBFactor	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",04)	CS	DiffusionDirection	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",06)	CS	ImageEnhanced	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",07)	CS	ImageTypeEDES	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",08)	IS	PhaseNumber	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",09)	FL	ImagePrepulseDelay	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",0a)	IS	SliceNumberMR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",0b)	CS	SliceOrientation	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",0c)	CS	ArrhythmiaRejection	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",0e)	CS	CardiacCycled	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",0f)	SS	CardiacGateWidth	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",10)	CS	CardiacSync	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",11)	FL	DiffusionEchoTime	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",12)	CS	DynamicSeries	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",13)	SL	EPIFactor	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",14)	SL	NumberOfEchoes	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",15)	SS	NumberOfLocations	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",16)	SS	NumberOfPCDirections	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",17)	SL	NumberOfPhasesMR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",18)	SL	NumberOfSlicesMR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",19)	CS	PartialMatrixScanned	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",1a)	FL	PCVelocity	1-n	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",1b)	FL	PrepulseDelay	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",1c)	CS	PrepulseType	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",1d)	IS	ReconstructionNumberMR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",1f)	CS	RespirationSync	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",20)	LO	ScanningTechnique	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",21)	CS	SPIR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",22)	FL	WaterFatShift	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",23)	DS	FlipAnglePhilips	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",24)	CS	SeriesIsInteractive	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",25)	SH	EchoTimeDisplayMR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",26)	CS	PresentationStateSubtractionActive	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",2d)	SS	StackNumberOfSlices	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",32)	FL	StackRadialAngle	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",33)	CS	StackRadialAxis	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",35)	SS	StackSliceNumber	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",36)	CS	StackType	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",3f)	CS	ZoomMode	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",58)	UL	ContrastTransferTaste	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",5f)	SQ	StackSequence	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",60)	SL	NumberOfStacks	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",61)	CS	SeriesTransmitted	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",62)	CS	SeriesCommitted	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",63)	CS	ExaminationSource	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",67)	CS	LinearPresentationGLTrafoShapeSub	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",77)	CS	GLTrafoType	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",7b)	IS	AcquisitionNumber	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",81)	IS	NumberOfDynamicScans	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",9f)	US	PixelProcessingKernelSize	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",a1)	CS	IsRawImage	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",f1)	FL	ProspectiveMotionCorrection	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",f2)	FL	RetrospectiveMotionCorrection	1	PrivateTag
+
+# Note: Some Philips devices use these private tags with reservation value
+# "Philips MR Imaging DD 001", others use "PHILIPS MR IMAGING DD 001". All attributes
+# should thus be present twice in this dictionary, once for each spelling variant.
+#
+(2005,"Philips MR Imaging DD 001",05)	CS	SynergyReconstructionType	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",1e)	SH	MIPProtocol	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",1f)	SH	MPRProtocol	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",20)	SL	NumberOfChemicalShifts	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",2d)	SS	NumberOfStackSlices	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",83)	SQ	Unknown	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",a1)	CS	SyncraScanType	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",b0)	FL	DiffusionDirectionRL	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",b1)	FL	DiffusionDirectionAP	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",b2)	FL	DiffusionDirectionFH	1	PrivateTag
+
+(2005,"Philips MR Imaging DD 005",02)	SQ	Unknown	1	PrivateTag
+
+# Note: Some Philips devices use these private tags with reservation value
+# "Philips MR Imaging DD 001", others use "PHILIPS MR IMAGING DD 001". All attributes
+# should thus be present twice in this dictionary, once for each spelling variant.
+#
+(2005,"PHILIPS MR IMAGING DD 001",05)	CS	SynergyReconstructionType	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",1e)	SH	MIPProtocol	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",1f)	SH	MPRProtocol	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",20)	SL	NumberOfChemicalShifts	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",2d)	SS	NumberOfStackSlices	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",83)	SQ	Unknown	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",a1)	CS	SyncraScanType	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",b0)	FL	DiffusionDirectionRL	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",b1)	FL	DiffusionDirectionAP	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",b2)	FL	DiffusionDirectionFH	1	PrivateTag
+
+(0019,"PHILIPS MR R5.5/PART",1000)	DS	FieldOfView	1	PrivateTag
+(0019,"PHILIPS MR R5.6/PART",1000)	DS	FieldOfView	1	PrivateTag
+
+(0019,"PHILIPS MR SPECTRO;1",01)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",02)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",03)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",04)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",05)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",06)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",07)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",08)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",09)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",10)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",12)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",13)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",14)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",15)	US	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",16)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",17)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",18)	UN	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",20)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",21)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",22)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",23)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",24)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",25)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",26)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",27)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",28)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",29)	IS	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",31)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",32)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",41)	LT	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",42)	IS	Unknown	2	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",43)	IS	Unknown	2	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",45)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",46)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",47)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",48)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",49)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",50)	UN	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",60)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",61)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",70)	UN	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",71)	IS	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",72)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",73)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",74)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",76)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",77)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",78)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",79)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",80)	IS	Unknown	1	PrivateTag
+
+(0009,"PHILIPS MR",10)	LO	SPIRelease	1	PrivateTag
+(0009,"PHILIPS MR",12)	LO	Unknown	1	PrivateTag
+
+(0019,"PHILIPS MR/LAST",09)	DS	MainMagneticField	1	PrivateTag
+(0019,"PHILIPS MR/LAST",0e)	IS	FlowCompensation	1	PrivateTag
+(0019,"PHILIPS MR/LAST",b1)	IS	MinimumRRInterval	1	PrivateTag
+(0019,"PHILIPS MR/LAST",b2)	IS	MaximumRRInterval	1	PrivateTag
+(0019,"PHILIPS MR/LAST",b3)	IS	NumberOfRejections	1	PrivateTag
+(0019,"PHILIPS MR/LAST",b4)	IS	NumberOfRRIntervals	1-n	PrivateTag
+(0019,"PHILIPS MR/LAST",b5)	IS	ArrhythmiaRejection	1	PrivateTag
+(0019,"PHILIPS MR/LAST",c0)	DS	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR/LAST",c6)	IS	CycledMultipleSlice	1	PrivateTag
+(0019,"PHILIPS MR/LAST",ce)	IS	REST	1	PrivateTag
+(0019,"PHILIPS MR/LAST",d5)	DS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/LAST",d6)	IS	FourierInterpolation	1	PrivateTag
+(0019,"PHILIPS MR/LAST",d9)	IS	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR/LAST",e0)	IS	Prepulse	1	PrivateTag
+(0019,"PHILIPS MR/LAST",e1)	DS	PrepulseDelay	1	PrivateTag
+(0019,"PHILIPS MR/LAST",e2)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/LAST",e3)	DS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/LAST",f0)	LT	WSProtocolString1	1	PrivateTag
+(0019,"PHILIPS MR/LAST",f1)	LT	WSProtocolString2	1	PrivateTag
+(0019,"PHILIPS MR/LAST",f2)	LT	WSProtocolString3	1	PrivateTag
+(0019,"PHILIPS MR/LAST",f3)	LT	WSProtocolString4	1	PrivateTag
+(0021,"PHILIPS MR/LAST",00)	IS	Unknown	1	PrivateTag
+(0021,"PHILIPS MR/LAST",10)	IS	Unknown	1	PrivateTag
+(0021,"PHILIPS MR/LAST",20)	IS	Unknown	1	PrivateTag
+(0021,"PHILIPS MR/LAST",21)	DS	SliceGap	1	PrivateTag
+(0021,"PHILIPS MR/LAST",22)	DS	StackRadialAngle	1	PrivateTag
+(0027,"PHILIPS MR/LAST",00)	US	Unknown	1	PrivateTag
+(0027,"PHILIPS MR/LAST",11)	US	Unknown	1-n	PrivateTag
+(0027,"PHILIPS MR/LAST",12)	DS	Unknown	1-n	PrivateTag
+(0027,"PHILIPS MR/LAST",13)	DS	Unknown	1-n	PrivateTag
+(0027,"PHILIPS MR/LAST",14)	DS	Unknown	1-n	PrivateTag
+(0027,"PHILIPS MR/LAST",15)	DS	Unknown	1-n	PrivateTag
+(0027,"PHILIPS MR/LAST",16)	LO	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/LAST",10)	DS	FPMin	1	PrivateTag
+(0029,"PHILIPS MR/LAST",20)	DS	FPMax	1	PrivateTag
+(0029,"PHILIPS MR/LAST",30)	DS	ScaledMinimum	1	PrivateTag
+(0029,"PHILIPS MR/LAST",40)	DS	ScaledMaximum	1	PrivateTag
+(0029,"PHILIPS MR/LAST",50)	DS	WindowMinimum	1	PrivateTag
+(0029,"PHILIPS MR/LAST",60)	DS	WindowMaximum	1	PrivateTag
+(0029,"PHILIPS MR/LAST",61)	IS	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/LAST",70)	DS	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/LAST",71)	DS	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/LAST",72)	IS	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/LAST",80)	IS	ViewCenter	1	PrivateTag
+(0029,"PHILIPS MR/LAST",81)	IS	ViewSize	1	PrivateTag
+(0029,"PHILIPS MR/LAST",82)	IS	ViewZoom	1	PrivateTag
+(0029,"PHILIPS MR/LAST",83)	IS	ViewTransform	1	PrivateTag
+(6001,"PHILIPS MR/LAST",00)	LT	Unknown	1	PrivateTag
+
+(0019,"PHILIPS MR/PART",1000)	DS	FieldOfView	1	PrivateTag
+(0019,"PHILIPS MR/PART",1005)	DS	CCAngulation	1	PrivateTag
+(0019,"PHILIPS MR/PART",1006)	DS	APAngulation	1	PrivateTag
+(0019,"PHILIPS MR/PART",1007)	DS	LRAngulation	1	PrivateTag
+(0019,"PHILIPS MR/PART",1008)	IS	PatientPosition	1	PrivateTag
+(0019,"PHILIPS MR/PART",1009)	IS	PatientOrientation	1	PrivateTag
+(0019,"PHILIPS MR/PART",100a)	IS	SliceOrientation	1	PrivateTag
+(0019,"PHILIPS MR/PART",100b)	DS	LROffcenter	1	PrivateTag
+(0019,"PHILIPS MR/PART",100c)	DS	CCOffcenter	1	PrivateTag
+(0019,"PHILIPS MR/PART",100d)	DS	APOffcenter	1	PrivateTag
+(0019,"PHILIPS MR/PART",100e)	DS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",100f)	IS	NumberOfSlices	1	PrivateTag
+(0019,"PHILIPS MR/PART",1010)	DS	SliceFactor	1	PrivateTag
+(0019,"PHILIPS MR/PART",1011)	DS	EchoTimes	1-n	PrivateTag
+(0019,"PHILIPS MR/PART",1015)	IS	DynamicStudy	1	PrivateTag
+(0019,"PHILIPS MR/PART",1018)	DS	HeartbeatInterval	1	PrivateTag
+(0019,"PHILIPS MR/PART",1019)	DS	RepetitionTimeFFE	1	PrivateTag
+(0019,"PHILIPS MR/PART",101a)	DS	FFEFlipAngle	1	PrivateTag
+(0019,"PHILIPS MR/PART",101b)	IS	NumberOfScans	1	PrivateTag
+(0019,"PHILIPS MR/PART",1021)	DS	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR/PART",1022)	DS	DynamicScanTimeBegin	1	PrivateTag
+(0019,"PHILIPS MR/PART",1024)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",1064)	DS	RepetitionTimeSE	1	PrivateTag
+(0019,"PHILIPS MR/PART",1065)	DS	RepetitionTimeIR	1	PrivateTag
+(0019,"PHILIPS MR/PART",1069)	IS	NumberOfPhases	1	PrivateTag
+(0019,"PHILIPS MR/PART",106a)	IS	CardiacFrequency	1	PrivateTag
+(0019,"PHILIPS MR/PART",106b)	DS	InversionDelay	1	PrivateTag
+(0019,"PHILIPS MR/PART",106c)	DS	GateDelay	1	PrivateTag
+(0019,"PHILIPS MR/PART",106d)	DS	GateWidth	1	PrivateTag
+(0019,"PHILIPS MR/PART",106e)	DS	TriggerDelayTime	1	PrivateTag
+(0019,"PHILIPS MR/PART",1080)	IS	NumberOfChemicalShifts	1	PrivateTag
+(0019,"PHILIPS MR/PART",1081)	DS	ChemicalShift	1	PrivateTag
+(0019,"PHILIPS MR/PART",1084)	IS	NumberOfRows	1	PrivateTag
+(0019,"PHILIPS MR/PART",1085)	IS	NumberOfSamples	1	PrivateTag
+(0019,"PHILIPS MR/PART",1094)	LO	MagnetizationTransferContrast	1	PrivateTag
+(0019,"PHILIPS MR/PART",1095)	LO	SpectralPresaturationWithInversionRecovery	1	PrivateTag
+(0019,"PHILIPS MR/PART",1096)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",1097)	LO	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10a0)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10a1)	DS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10a3)	DS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10a4)	CS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10c8)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10c9)	IS	FoldoverDirectionTransverse	1	PrivateTag
+(0019,"PHILIPS MR/PART",10ca)	IS	FoldoverDirectionSagittal	1	PrivateTag
+(0019,"PHILIPS MR/PART",10cb)	IS	FoldoverDirectionCoronal	1	PrivateTag
+(0019,"PHILIPS MR/PART",10cc)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10cd)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10ce)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10cf)	IS	NumberOfEchoes	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d0)	IS	ScanResolution	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d2)	LO	WaterFatShift	2	PrivateTag
+(0019,"PHILIPS MR/PART",10d4)	IS	ArtifactReduction	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d5)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d6)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d7)	DS	ScanPercentage	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d8)	IS	Halfscan	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d9)	IS	EPIFactor	1	PrivateTag
+(0019,"PHILIPS MR/PART",10da)	IS	TurboFactor	1	PrivateTag
+(0019,"PHILIPS MR/PART",10db)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10e0)	IS	PercentageOfScanCompleted	1	PrivateTag
+(0019,"PHILIPS MR/PART",10e1)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",1100)	IS	NumberOfStacks	1	PrivateTag
+(0019,"PHILIPS MR/PART",1101)	IS	StackType	1-n	PrivateTag
+(0019,"PHILIPS MR/PART",1102)	IS	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR/PART",110b)	DS	LROffcenter	1	PrivateTag
+(0019,"PHILIPS MR/PART",110c)	DS	CCOffcenter	1	PrivateTag
+(0019,"PHILIPS MR/PART",110d)	DS	APOffcenter	1	PrivateTag
+(0019,"PHILIPS MR/PART",1145)	IS	ReconstructionResolution	1	PrivateTag
+(0019,"PHILIPS MR/PART",11fc)	IS	ResonanceFrequency	1	PrivateTag
+(0019,"PHILIPS MR/PART",12c0)	DS	TriggerDelayTimes	1	PrivateTag
+(0019,"PHILIPS MR/PART",12e0)	IS	PrepulseType	1	PrivateTag
+(0019,"PHILIPS MR/PART",12e1)	DS	PrepulseDelay	1	PrivateTag
+(0019,"PHILIPS MR/PART",12e3)	DS	PhaseContrastVelocity	1	PrivateTag
+(0021,"PHILIPS MR/PART",1000)	IS	ReconstructionNumber	1	PrivateTag
+(0021,"PHILIPS MR/PART",1010)	IS	ImageType	1	PrivateTag
+(0021,"PHILIPS MR/PART",1020)	IS	SliceNumber	1	PrivateTag
+(0021,"PHILIPS MR/PART",1030)	IS	EchoNumber	1	PrivateTag
+(0021,"PHILIPS MR/PART",1031)	DS	PatientReferenceID	1	PrivateTag
+(0021,"PHILIPS MR/PART",1035)	IS	ChemicalShiftNumber	1	PrivateTag
+(0021,"PHILIPS MR/PART",1040)	IS	PhaseNumber	1	PrivateTag
+(0021,"PHILIPS MR/PART",1050)	IS	DynamicScanNumber	1	PrivateTag
+(0021,"PHILIPS MR/PART",1060)	IS	NumberOfRowsInObject	1	PrivateTag
+(0021,"PHILIPS MR/PART",1061)	IS	RowNumber	1-n	PrivateTag
+(0021,"PHILIPS MR/PART",1062)	IS	Unknown	1-n	PrivateTag
+(0021,"PHILIPS MR/PART",1100)	DA	ScanDate	1	PrivateTag
+(0021,"PHILIPS MR/PART",1110)	TM	ScanTime	1	PrivateTag
+(0021,"PHILIPS MR/PART",1221)	IS	SliceGap	1	PrivateTag
+(0029,"PHILIPS MR/PART",00)	DS	Unknown	2	PrivateTag
+(0029,"PHILIPS MR/PART",04)	US	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/PART",10)	DS	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/PART",11)	DS	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/PART",20)	LO	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/PART",31)	DS	Unknown	2	PrivateTag
+(0029,"PHILIPS MR/PART",32)	DS	Unknown	2	PrivateTag
+(0029,"PHILIPS MR/PART",c3)	IS	ScanResolution	1	PrivateTag
+(0029,"PHILIPS MR/PART",c4)	IS	FieldOfView	1	PrivateTag
+(0029,"PHILIPS MR/PART",d5)	LT	SliceThickness	1	PrivateTag
+
+(0019,"PHILIPS-MR-1",11)	IS	ChemicalShiftNumber	1	PrivateTag
+(0019,"PHILIPS-MR-1",12)	IS	PhaseNumber	1	PrivateTag
+(0021,"PHILIPS-MR-1",01)	IS	ReconstructionNumber	1	PrivateTag
+(0021,"PHILIPS-MR-1",02)	IS	SliceNumber	1	PrivateTag
+
+(7001,"Picker NM Private Group",01)	UI	Unknown	1	PrivateTag
+(7001,"Picker NM Private Group",02)	OB	Unknown	1	PrivateTag
+
+(0019,"SIEMENS CM VA0  ACQU",10)	LT	ParameterFileName	1	PrivateTag
+(0019,"SIEMENS CM VA0  ACQU",11)	LO	SequenceFileName	1	PrivateTag
+(0019,"SIEMENS CM VA0  ACQU",12)	LT	SequenceFileOwner	1	PrivateTag
+(0019,"SIEMENS CM VA0  ACQU",13)	LT	SequenceDescription	1	PrivateTag
+(0019,"SIEMENS CM VA0  ACQU",14)	LT	EPIFileName	1	PrivateTag
+
+(0009,"SIEMENS CM VA0  CMS",00)	DS	NumberOfMeasurements	1	PrivateTag
+(0009,"SIEMENS CM VA0  CMS",10)	LT	StorageMode	1	PrivateTag
+(0009,"SIEMENS CM VA0  CMS",12)	UL	EvaluationMaskImage	1	PrivateTag
+(0009,"SIEMENS CM VA0  CMS",26)	DA	LastMoveDate	1	PrivateTag
+(0009,"SIEMENS CM VA0  CMS",27)	TM	LastMoveTime	1	PrivateTag
+(0011,"SIEMENS CM VA0  CMS",0a)	LT	Unknown	1	PrivateTag
+(0011,"SIEMENS CM VA0  CMS",10)	DA	RegistrationDate	1	PrivateTag
+(0011,"SIEMENS CM VA0  CMS",11)	TM	RegistrationTime	1	PrivateTag
+(0011,"SIEMENS CM VA0  CMS",22)	LT	Unknown	1	PrivateTag
+(0011,"SIEMENS CM VA0  CMS",23)	DS	UsedPatientWeight	1	PrivateTag
+(0011,"SIEMENS CM VA0  CMS",40)	IS	OrganCode	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",00)	LT	ModifyingPhysician	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",10)	DA	ModificationDate	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",12)	TM	ModificationTime	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",20)	LO	PatientName	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",22)	LO	PatientId	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",30)	DA	PatientBirthdate	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",31)	DS	PatientWeight	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",32)	LT	PatientsMaidenName	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",33)	LT	ReferringPhysician	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",34)	LT	AdmittingDiagnosis	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",35)	LO	PatientSex	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",40)	LO	ProcedureDescription	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",42)	LO	RestDirection	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",44)	LO	PatientPosition	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",46)	LT	ViewDirection	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",50)	LT	Unknown	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",51)	LT	Unknown	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",52)	LT	Unknown	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",53)	LT	Unknown	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",54)	LT	Unknown	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",55)	LT	Unknown	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",56)	LT	Unknown	1	PrivateTag
+(0019,"SIEMENS CM VA0  CMS",10)	DS	NetFrequency	1	PrivateTag
+(0019,"SIEMENS CM VA0  CMS",20)	LT	MeasurementMode	1	PrivateTag
+(0019,"SIEMENS CM VA0  CMS",30)	LT	CalculationMode	1	PrivateTag
+(0019,"SIEMENS CM VA0  CMS",50)	IS	NoiseLevel	1	PrivateTag
+(0019,"SIEMENS CM VA0  CMS",60)	IS	NumberOfDataBytes	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",20)	DS	FoV	2	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",22)	DS	ImageMagnificationFactor	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",24)	DS	ImageScrollOffset	2	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",26)	IS	ImagePixelOffset	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",30)	LT	ViewDirection	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",32)	CS	PatientRestDirection	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",60)	DS	ImagePosition	3	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",61)	DS	ImageNormal	3	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",63)	DS	ImageDistance	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",65)	US	ImagePositioningHistoryMask	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",6a)	DS	ImageRow	3	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",6b)	DS	ImageColumn	3	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",70)	LT	PatientOrientationSet1	3	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",71)	LT	PatientOrientationSet2	3	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",80)	LT	StudyName	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",82)	LT	StudyType	3	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",10)	LT	WindowStyle	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",11)	LT	Unknown	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",13)	LT	Unknown	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",20)	LT	PixelQualityCode	3	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",22)	IS	PixelQualityValue	3	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",50)	LT	ArchiveCode	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",51)	LT	ExposureCode	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",52)	LT	SortCode	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",53)	LT	Unknown	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",60)	LT	Splash	1	PrivateTag
+(0051,"SIEMENS CM VA0  CMS",10)	LT	ImageText	1-n	PrivateTag
+(6021,"SIEMENS CM VA0  CMS",00)	LT	ImageGraphicsFormatCode	1	PrivateTag
+(6021,"SIEMENS CM VA0  CMS",10)	LT	ImageGraphics	1	PrivateTag
+(7fe1,"SIEMENS CM VA0  CMS",00)	OB	BinaryData	1-n	PrivateTag
+
+(0009,"SIEMENS CM VA0  LAB",10)	LT	GeneratorIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",11)	LT	GantryIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",12)	LT	X-RayTubeIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",13)	LT	DetectorIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",14)	LT	DASIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",15)	LT	SMIIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",16)	LT	CPUIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",20)	LT	HeaderVersion	1	PrivateTag
+
+(0029,"SIEMENS CSA HEADER",08)	CS	CSAImageHeaderType	1	PrivateTag
+(0029,"SIEMENS CSA HEADER",09)	LO	CSAImageHeaderVersion	1	PrivateTag
+(0029,"SIEMENS CSA HEADER",10)	OB	CSAImageHeaderInfo	1	PrivateTag
+(0029,"SIEMENS CSA HEADER",18)	CS	CSASeriesHeaderType	1	PrivateTag
+(0029,"SIEMENS CSA HEADER",19)	LO	CSASeriesHeaderVersion	1	PrivateTag
+(0029,"SIEMENS CSA HEADER",20)	OB	CSASeriesHeaderInfo	1	PrivateTag
+
+(0029,"SIEMENS CSA NON-IMAGE",08)	CS	CSADataType	1	PrivateTag
+(0029,"SIEMENS CSA NON-IMAGE",09)	LO	CSADataVersion	1	PrivateTag
+(0029,"SIEMENS CSA NON-IMAGE",10)	OB	CSADataInfo	1	PrivateTag
+(7FE1,"SIEMENS CSA NON-IMAGE",10)	OB	CSAData	1	PrivateTag
+
+(0019,"SIEMENS CT VA0  COAD",10)	DS	DistanceSourceToSourceSideCollimator	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",11)	DS	DistanceSourceToDetectorSideCollimator	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",20)	IS	NumberOfPossibleChannels	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",21)	IS	MeanChannelNumber	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",22)	DS	DetectorSpacing	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",23)	DS	DetectorCenter	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",24)	DS	ReadingIntegrationTime	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",50)	DS	DetectorAlignment	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",52)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",54)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",60)	DS	FocusAlignment	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",65)	UL	FocalSpotDeflectionAmplitude	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",66)	UL	FocalSpotDeflectionPhase	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",67)	UL	FocalSpotDeflectionOffset	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",70)	DS	WaterScalingFactor	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",71)	DS	InterpolationFactor	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",80)	LT	PatientRegion	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",82)	LT	PatientPhaseOfLife	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",90)	DS	OsteoOffset	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",92)	DS	OsteoRegressionLineSlope	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",93)	DS	OsteoRegressionLineIntercept	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",94)	DS	OsteoStandardizationCode	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",96)	IS	OsteoPhantomNumber	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A3)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A4)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A5)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A6)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A7)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A8)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A9)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",AA)	LT	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",AB)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",AC)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",AD)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",AE)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",AF)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",B0)	DS	FeedPerRotation	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",BD)	IS	PulmoTriggerLevel	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",BE)	DS	ExpiratoricReserveVolume	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",BF)	DS	VitalCapacity	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",C0)	DS	PulmoWater	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",C1)	DS	PulmoAir	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",C2)	DA	PulmoDate	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",C3)	TM	PulmoTime	1	PrivateTag
+
+(0019,"SIEMENS CT VA0  GEN",10)	DS	SourceSideCollimatorAperture	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",11)	DS	DetectorSideCollimatorAperture	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",20)	DS	ExposureTime	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",21)	DS	ExposureCurrent	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",25)	DS	KVPGeneratorPowerCurrent	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",26)	DS	GeneratorVoltage	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",40)	UL	MasterControlMask	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",42)	US	ProcessingMask	5	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",44)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",45)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",62)	IS	NumberOfVirtuellChannels	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",70)	IS	NumberOfReadings	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",71)	LT	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",74)	IS	NumberOfProjections	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",75)	IS	NumberOfBytes	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",80)	LT	ReconstructionAlgorithmSet	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",81)	LT	ReconstructionAlgorithmIndex	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",82)	LT	RegenerationSoftwareVersion	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",88)	DS	Unknown	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",10)	IS	RotationAngle	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",11)	IS	StartAngle	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",20)	US	Unknown	1-n	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",30)	IS	TopogramTubePosition	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",32)	DS	LengthOfTopogram	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",34)	DS	TopogramCorrectionFactor	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",36)	DS	MaximumTablePosition	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",40)	IS	TableMoveDirectionCode	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",45)	IS	VOIStartRow	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",46)	IS	VOIStopRow	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",47)	IS	VOIStartColumn	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",48)	IS	VOIStopColumn	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",49)	IS	VOIStartSlice	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",4a)	IS	VOIStopSlice	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",50)	IS	VectorStartRow	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",51)	IS	VectorRowStep	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",52)	IS	VectorStartColumn	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",53)	IS	VectorColumnStep	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",60)	IS	RangeTypeCode	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",62)	IS	ReferenceTypeCode	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",70)	DS	ObjectOrientation	3	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",72)	DS	LightOrientation	3	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",75)	DS	LightBrightness	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",76)	DS	LightContrast	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",7a)	IS	OverlayThreshold	2	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",7b)	IS	SurfaceThreshold	2	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",7c)	IS	GreyScaleThreshold	2	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",a0)	DS	Unknown	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",a2)	LT	Unknown	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",a7)	LT	Unknown	1	PrivateTag
+
+(0009,"SIEMENS CT VA0  IDE",10)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",30)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",31)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",32)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",34)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",40)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",42)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",50)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",51)	LT	Unknown	1	PrivateTag
+
+(0009,"SIEMENS CT VA0  ORI",20)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  ORI",30)	LT	Unknown	1	PrivateTag
+
+(6021,"SIEMENS CT VA0  OST",00)	LT	OsteoContourComment	1	PrivateTag
+(6021,"SIEMENS CT VA0  OST",10)	US	OsteoContourBuffer	256	PrivateTag
+
+(0021,"SIEMENS CT VA0  RAW",10)	UL	CreationMask	2	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",20)	UL	EvaluationMask	2	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",30)	US	ExtendedProcessingMask	7	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",40)	US	Unknown	1-n	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",41)	US	Unknown	1-n	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",42)	US	Unknown	1-n	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",43)	US	Unknown	1-n	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",44)	US	Unknown	1-n	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",50)	LT	Unknown	1	PrivateTag
+
+(0009,"SIEMENS DICOM",10)	UN	Unknown	1	PrivateTag
+(0009,"SIEMENS DICOM",12)	LT	Unknown	1	PrivateTag
+
+(0019,"SIEMENS DLR.01",10)	LT	MeasurementMode	1	PrivateTag
+(0019,"SIEMENS DLR.01",11)	LT	ImageType	1	PrivateTag
+(0019,"SIEMENS DLR.01",15)	LT	SoftwareVersion	1	PrivateTag
+(0019,"SIEMENS DLR.01",20)	LT	MPMCode	1	PrivateTag
+(0019,"SIEMENS DLR.01",21)	LT	Latitude	1	PrivateTag
+(0019,"SIEMENS DLR.01",22)	LT	Sensitivity	1	PrivateTag
+(0019,"SIEMENS DLR.01",23)	LT	EDR	1	PrivateTag
+(0019,"SIEMENS DLR.01",24)	LT	LFix	1	PrivateTag
+(0019,"SIEMENS DLR.01",25)	LT	SFix	1	PrivateTag
+(0019,"SIEMENS DLR.01",26)	LT	PresetMode	1	PrivateTag
+(0019,"SIEMENS DLR.01",27)	LT	Region	1	PrivateTag
+(0019,"SIEMENS DLR.01",28)	LT	Subregion	1	PrivateTag
+(0019,"SIEMENS DLR.01",30)	LT	Orientation	1	PrivateTag
+(0019,"SIEMENS DLR.01",31)	LT	MarkOnFilm	1	PrivateTag
+(0019,"SIEMENS DLR.01",32)	LT	RotationOnDRC	1	PrivateTag
+(0019,"SIEMENS DLR.01",40)	LT	ReaderType	1	PrivateTag
+(0019,"SIEMENS DLR.01",41)	LT	SubModality	1	PrivateTag
+(0019,"SIEMENS DLR.01",42)	LT	ReaderSerialNumber	1	PrivateTag
+(0019,"SIEMENS DLR.01",50)	LT	CassetteScale	1	PrivateTag
+(0019,"SIEMENS DLR.01",51)	LT	CassetteMatrix	1	PrivateTag
+(0019,"SIEMENS DLR.01",52)	LT	CassetteSubmatrix	1	PrivateTag
+(0019,"SIEMENS DLR.01",53)	LT	Barcode	1	PrivateTag
+(0019,"SIEMENS DLR.01",60)	LT	ContrastType	1	PrivateTag
+(0019,"SIEMENS DLR.01",61)	LT	RotationAmount	1	PrivateTag
+(0019,"SIEMENS DLR.01",62)	LT	RotationCenter	1	PrivateTag
+(0019,"SIEMENS DLR.01",63)	LT	DensityShift	1	PrivateTag
+(0019,"SIEMENS DLR.01",64)	US	FrequencyRank	1	PrivateTag
+(0019,"SIEMENS DLR.01",65)	LT	FrequencyEnhancement	1	PrivateTag
+(0019,"SIEMENS DLR.01",66)	LT	FrequencyType	1	PrivateTag
+(0019,"SIEMENS DLR.01",67)	LT	KernelLength	1	PrivateTag
+(0019,"SIEMENS DLR.01",68)	UL	KernelMode	1	PrivateTag
+(0019,"SIEMENS DLR.01",69)	UL	ConvolutionMode	1	PrivateTag
+(0019,"SIEMENS DLR.01",70)	LT	PLASource	1	PrivateTag
+(0019,"SIEMENS DLR.01",71)	LT	PLADestination	1	PrivateTag
+(0019,"SIEMENS DLR.01",75)	LT	UIDOriginalImage	1	PrivateTag
+(0019,"SIEMENS DLR.01",76)	LT	Unknown	1	PrivateTag
+(0019,"SIEMENS DLR.01",80)	LT	ReaderHeader	1	PrivateTag
+(0019,"SIEMENS DLR.01",90)	LT	PLAOfSecondaryDestination	1	PrivateTag
+(0019,"SIEMENS DLR.01",a0)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS DLR.01",a1)	DS	Unknown	1	PrivateTag
+(0041,"SIEMENS DLR.01",10)	US	NumberOfHardcopies	1	PrivateTag
+(0041,"SIEMENS DLR.01",20)	LT	FilmFormat	1	PrivateTag
+(0041,"SIEMENS DLR.01",30)	LT	FilmSize	1	PrivateTag
+(0041,"SIEMENS DLR.01",31)	LT	FullFilmFormat	1	PrivateTag
+
+(0003,"SIEMENS ISI",08)	US	ISICommandField	1	PrivateTag
+(0003,"SIEMENS ISI",11)	US	AttachIDApplicationCode	1	PrivateTag
+(0003,"SIEMENS ISI",12)	UL	AttachIDMessageCount	1	PrivateTag
+(0003,"SIEMENS ISI",13)	DA	AttachIDDate	1	PrivateTag
+(0003,"SIEMENS ISI",14)	TM	AttachIDTime	1	PrivateTag
+(0003,"SIEMENS ISI",20)	US	MessageType	1	PrivateTag
+(0003,"SIEMENS ISI",30)	DA	MaxWaitingDate	1	PrivateTag
+(0003,"SIEMENS ISI",31)	TM	MaxWaitingTime	1	PrivateTag
+(0009,"SIEMENS ISI",01)	UN	RISPatientInfoIMGEF	1	PrivateTag
+(0011,"SIEMENS ISI",03)	LT	PatientUID	1	PrivateTag
+(0011,"SIEMENS ISI",04)	LT	PatientID	1	PrivateTag
+(0011,"SIEMENS ISI",0a)	LT	CaseID	1	PrivateTag
+(0011,"SIEMENS ISI",22)	LT	RequestID	1	PrivateTag
+(0011,"SIEMENS ISI",23)	LT	ExaminationUID	1	PrivateTag
+(0011,"SIEMENS ISI",a1)	DA	PatientRegistrationDate	1	PrivateTag
+(0011,"SIEMENS ISI",a2)	TM	PatientRegistrationTime	1	PrivateTag
+(0011,"SIEMENS ISI",b0)	LT	PatientLastName	1	PrivateTag
+(0011,"SIEMENS ISI",b2)	LT	PatientFirstName	1	PrivateTag
+(0011,"SIEMENS ISI",b4)	LT	PatientHospitalStatus	1	PrivateTag
+(0011,"SIEMENS ISI",bc)	TM	CurrentLocationTime	1	PrivateTag
+(0011,"SIEMENS ISI",c0)	LT	PatientInsuranceStatus	1	PrivateTag
+(0011,"SIEMENS ISI",d0)	LT	PatientBillingType	1	PrivateTag
+(0011,"SIEMENS ISI",d2)	LT	PatientBillingAddress	1	PrivateTag
+(0031,"SIEMENS ISI",12)	LT	ExaminationReason	1	PrivateTag
+(0031,"SIEMENS ISI",30)	DA	RequestedDate	1	PrivateTag
+(0031,"SIEMENS ISI",32)	TM	WorklistRequestStartTime	1	PrivateTag
+(0031,"SIEMENS ISI",33)	TM	WorklistRequestEndTime	1	PrivateTag
+(0031,"SIEMENS ISI",4a)	TM	RequestedTime	1	PrivateTag
+(0031,"SIEMENS ISI",80)	LT	RequestedLocation	1	PrivateTag
+(0055,"SIEMENS ISI",46)	LT	CurrentWard	1	PrivateTag
+(0193,"SIEMENS ISI",02)	DS	RISKey	1	PrivateTag
+(0307,"SIEMENS ISI",01)	UN	RISWorklistIMGEF	1	PrivateTag
+(0309,"SIEMENS ISI",01)	UN	RISReportIMGEF	1	PrivateTag
+(4009,"SIEMENS ISI",01)	LT	ReportID	1	PrivateTag
+(4009,"SIEMENS ISI",20)	LT	ReportStatus	1	PrivateTag
+(4009,"SIEMENS ISI",30)	DA	ReportCreationDate	1	PrivateTag
+(4009,"SIEMENS ISI",70)	LT	ReportApprovingPhysician	1	PrivateTag
+(4009,"SIEMENS ISI",e0)	LT	ReportText	1	PrivateTag
+(4009,"SIEMENS ISI",e1)	LT	ReportAuthor	1	PrivateTag
+(4009,"SIEMENS ISI",e3)	LT	ReportingRadiologist	1	PrivateTag
+
+(0029,"SIEMENS MED DISPLAY",04)	LT	PhotometricInterpretation	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",10)	US	RowsOfSubmatrix	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",11)	US	ColumnsOfSubmatrix	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",20)	US	Unknown	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",21)	US	Unknown	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",50)	US	OriginOfSubmatrix	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",99)	LT	ShutterType	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",a0)	US	RowsOfRectangularShutter	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",a1)	US	ColumnsOfRectangularShutter	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",a2)	US	OriginOfRectangularShutter	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",b0)	US	RadiusOfCircularShutter	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",b2)	US	OriginOfCircularShutter	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",c1)	US	ContourOfIrregularShutter	1	PrivateTag
+
+(0029,"SIEMENS MED HG",10)	US	ListOfGroupNumbers	1	PrivateTag
+(0029,"SIEMENS MED HG",15)	LT	ListOfShadowOwnerCodes	1	PrivateTag
+(0029,"SIEMENS MED HG",20)	US	ListOfElementNumbers	1	PrivateTag
+(0029,"SIEMENS MED HG",30)	US	ListOfTotalDisplayLength	1	PrivateTag
+(0029,"SIEMENS MED HG",40)	LT	ListOfDisplayPrefix	1	PrivateTag
+(0029,"SIEMENS MED HG",50)	LT	ListOfDisplayPostfix	1	PrivateTag
+(0029,"SIEMENS MED HG",60)	US	ListOfTextPosition	1	PrivateTag
+(0029,"SIEMENS MED HG",70)	LT	ListOfTextConcatenation	1	PrivateTag
+(0029,"SIEMENS MED MG",10)	US	ListOfGroupNumbers	1	PrivateTag
+(0029,"SIEMENS MED MG",15)	LT	ListOfShadowOwnerCodes	1	PrivateTag
+(0029,"SIEMENS MED MG",20)	US	ListOfElementNumbers	1	PrivateTag
+(0029,"SIEMENS MED MG",30)	US	ListOfTotalDisplayLength	1	PrivateTag
+(0029,"SIEMENS MED MG",40)	LT	ListOfDisplayPrefix	1	PrivateTag
+(0029,"SIEMENS MED MG",50)	LT	ListOfDisplayPostfix	1	PrivateTag
+(0029,"SIEMENS MED MG",60)	US	ListOfTextPosition	1	PrivateTag
+(0029,"SIEMENS MED MG",70)	LT	ListOfTextConcatenation	1	PrivateTag
+
+(0009,"SIEMENS MED",10)	LO	RecognitionCode	1	PrivateTag
+(0009,"SIEMENS MED",30)	UL	ByteOffsetOfOriginalHeader	1	PrivateTag
+(0009,"SIEMENS MED",31)	UL	LengthOfOriginalHeader	1	PrivateTag
+(0009,"SIEMENS MED",40)	UL	ByteOffsetOfPixelmatrix	1	PrivateTag
+(0009,"SIEMENS MED",41)	UL	LengthOfPixelmatrixInBytes	1	PrivateTag
+(0009,"SIEMENS MED",50)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS MED",51)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS MED",f5)	LT	PDMEFIDPlaceholder	1	PrivateTag
+(0009,"SIEMENS MED",f6)	LT	PDMDataObjectTypeExtension	1	PrivateTag
+(0021,"SIEMENS MED",10)	DS	Zoom	1	PrivateTag
+(0021,"SIEMENS MED",11)	DS	Target	2	PrivateTag
+(0021,"SIEMENS MED",12)	IS	TubeAngle	1	PrivateTag
+(0021,"SIEMENS MED",20)	US	ROIMask	1	PrivateTag
+(7001,"SIEMENS MED",10)	LT	Dummy	1	PrivateTag
+(7003,"SIEMENS MED",10)	LT	Header	1	PrivateTag
+(7005,"SIEMENS MED",10)	LT	Dummy	1	PrivateTag
+
+(0029,"SIEMENS MEDCOM HEADER",08)	CS	MedComHeaderType	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",09)	LO	MedComHeaderVersion	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",10)	OB	MedComHeaderInfo	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",20)	OB	MedComHistoryInformation	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",31)	LO	PMTFInformation1	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",32)	UL	PMTFInformation2	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",33)	UL	PMTFInformation3	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",34)	CS	PMTFInformation4	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",35)	UL	PMTFInformation5	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",40)	SQ	ApplicationHeaderSequence	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",41)	CS	ApplicationHeaderType	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",42)	LO	ApplicationHeaderID	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",43)	LO	ApplicationHeaderVersion	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",44)	OB	ApplicationHeaderInfo	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",50)	LO	WorkflowControlFlags	8	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",51)	CS	ArchiveManagementFlagKeepOnline	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",52)	CS	ArchiveManagementFlagDoNotArchive	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",53)	CS	ImageLocationStatus	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",54)	DS	EstimatedRetrieveTime	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",55)	DS	DataSizeOfRetrievedImages	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",70)	SQ	SiemensLinkSequence	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",71)	AT	ReferencedTag	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",72)	CS	ReferencedTagType	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",73)	UL	ReferencedValueLength	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",74)	CS	ReferencedObjectDeviceType	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",75)	OB	ReferencedObjectDeviceLocation	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",76)	OB	ReferencedObjectDeviceID	1	PrivateTag
+
+(0029,"SIEMENS MEDCOM HEADER2",60)	LO	SeriesWorkflowStatus	1	PrivateTag
+
+(0029,"SIEMENS MEDCOM OOG",08)	CS	MEDCOMOOGType	1	PrivateTag
+(0029,"SIEMENS MEDCOM OOG",09)	LO	MEDCOMOOGVersion	1	PrivateTag
+(0029,"SIEMENS MEDCOM OOG",10)	OB	MEDCOMOOGInfo	1	PrivateTag
+
+(0019,"SIEMENS MR VA0  COAD",12)	DS	MagneticFieldStrength	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",14)	DS	ADCVoltage	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",16)	DS	ADCOffset	2	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",20)	DS	TransmitterAmplitude	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",21)	IS	NumberOfTransmitterAmplitudes	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",22)	DS	TransmitterAttenuator	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",24)	DS	TransmitterCalibration	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",26)	DS	TransmitterReference	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",50)	DS	ReceiverTotalGain	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",51)	DS	ReceiverAmplifierGain	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",52)	DS	ReceiverPreamplifierGain	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",54)	DS	ReceiverCableAttenuation	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",55)	DS	ReceiverReferenceGain	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",56)	DS	ReceiverFilterFrequency	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",60)	DS	ReconstructionScaleFactor	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",62)	DS	ReferenceScaleFactor	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",70)	DS	PhaseGradientAmplitude	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",71)	DS	ReadoutGradientAmplitude	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",72)	DS	SelectionGradientAmplitude	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",80)	DS	GradientDelayTime	3	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",82)	DS	TotalGradientDelayTime	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",90)	LT	SensitivityCorrectionLabel	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",91)	DS	SaturationPhaseEncodingVectorCoronalComponent	6	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",92)	DS	SaturationReadoutVectorCoronalComponent	6	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",a0)	US	RFWatchdogMask	3	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",a1)	DS	EPIReconstructionSlope	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",a2)	DS	RFPowerErrorIndicator	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",a5)	DS	SpecificAbsorptionRateWholeBody	3	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",a6)	DS	SpecificEnergyDose	3	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",b0)	UL	AdjustmentStatusMask	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",c1)	DS	EPICapacity	6	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",c2)	DS	EPIInductance	3	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",c3)	IS	EPISwitchConfigurationCode	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",c4)	IS	EPISwitchHardwareCode	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",c5)	DS	EPISwitchDelayTime	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d1)	DS	FlowSensitivity	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d2)	LT	CalculationSubmode	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d3)	DS	FieldOfViewRatio	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d4)	IS	BaseRawMatrixSize	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d5)	IS	2DOversamplingLines	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d6)	IS	3DPhaseOversamplingPartitions	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d7)	IS	EchoLinePosition	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d8)	IS	EchoColumnPosition	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d9)	IS	LinesPerSegment	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",da)	LT	PhaseCodingDirection	1	PrivateTag
+
+(0019,"SIEMENS MR VA0  GEN",10)	DS	TotalMeasurementTimeNominal	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",11)	DS	TotalMeasurementTimeCurrent	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",12)	DS	StartDelayTime	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",13)	DS	DwellTime	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",14)	IS	NumberOfPhases	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",16)	UL	SequenceControlMask	2	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",18)	UL	MeasurementStatusMask	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",20)	IS	NumberOfFourierLinesNominal	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",21)	IS	NumberOfFourierLinesCurrent	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",26)	IS	NumberOfFourierLinesAfterZero	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",28)	IS	FirstMeasuredFourierLine	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",30)	IS	AcquisitionColumns	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",31)	IS	ReconstructionColumns	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",40)	IS	ArrayCoilElementNumber	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",41)	UL	ArrayCoilElementSelectMask	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",42)	UL	ArrayCoilElementDataMask	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",43)	IS	ArrayCoilElementToADCConnect	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",44)	DS	ArrayCoilElementNoiseLevel	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",45)	IS	ArrayCoilADCPairNumber	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",46)	UL	ArrayCoilCombinationMask	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",50)	IS	NumberOfAverages	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",60)	DS	FlipAngle	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",70)	IS	NumberOfPrescans	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",81)	LT	FilterTypeForRawData	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",82)	DS	FilterParameterForRawData	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",83)	LT	FilterTypeForImageData	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",84)	DS	FilterParameterForImageData	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",85)	LT	FilterTypeForPhaseCorrection	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",86)	DS	FilterParameterForPhaseCorrection	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",87)	LT	NormalizationFilterTypeForImageData	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",88)	DS	NormalizationFilterParameterForImageData	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",90)	IS	NumberOfSaturationRegions	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",91)	DS	SaturationPhaseEncodingVectorSagittalComponent	6	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",92)	DS	SaturationReadoutVectorSagittalComponent	6	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",93)	DS	EPIStimulationMonitorMode	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",94)	DS	ImageRotationAngle	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",96)	UL	CoilIDMask	3	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",97)	UL	CoilClassMask	2	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",98)	DS	CoilPosition	3	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",a0)	DS	EPIReconstructionPhase	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",a1)	DS	EPIReconstructionSlope	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",20)	IS	PhaseCorrectionRowsSequence	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",21)	IS	PhaseCorrectionColumnsSequence	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",22)	IS	PhaseCorrectionRowsReconstruction	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",24)	IS	PhaseCorrectionColumnsReconstruction	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",30)	IS	NumberOf3DRawPartitionsNominal	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",31)	IS	NumberOf3DRawPartitionsCurrent	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",34)	IS	NumberOf3DImagePartitions	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",36)	IS	Actual3DImagePartitionNumber	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",39)	DS	SlabThickness	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",40)	IS	NumberOfSlicesNominal	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",41)	IS	NumberOfSlicesCurrent	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",42)	IS	CurrentSliceNumber	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",43)	IS	CurrentGroupNumber	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",44)	DS	CurrentSliceDistanceFactor	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",45)	IS	MIPStartRow	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",46)	IS	MIPStopRow	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",47)	IS	MIPStartColumn	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",48)	IS	MIPStartColumn	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",49)	IS	MIPStartSlice Name=	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",4a)	IS	MIPStartSlice	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",4f)	LT	OrderofSlices	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",50)	US	SignalMask	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",52)	DS	DelayAfterTrigger	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",53)	IS	RRInterval	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",54)	DS	NumberOfTriggerPulses	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",56)	DS	RepetitionTimeEffective	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",57)	LT	GatePhase	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",58)	DS	GateThreshold	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",59)	DS	GatedRatio	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",60)	IS	NumberOfInterpolatedImages	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",70)	IS	NumberOfEchoes	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",72)	DS	SecondEchoTime	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",73)	DS	SecondRepetitionTime	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",80)	IS	CardiacCode	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",91)	DS	SaturationPhaseEncodingVectorTransverseComponent	6	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",92)	DS	SaturationReadoutVectorTransverseComponent	6	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",93)	DS	EPIChangeValueOfMagnitude	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",94)	DS	EPIChangeValueOfXComponent	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",95)	DS	EPIChangeValueOfYComponent	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",96)	DS	EPIChangeValueOfZComponent	1	PrivateTag
+
+(0021,"SIEMENS MR VA0  RAW",00)	LT	SequenceType	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",01)	IS	VectorSizeOriginal	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",02)	IS	VectorSizeExtended	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",03)	DS	AcquiredSpectralRange	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",04)	DS	VOIPosition	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",05)	DS	VOISize	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",06)	IS	CSIMatrixSizeOriginal	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",07)	IS	CSIMatrixSizeExtended	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",08)	DS	SpatialGridShift	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",09)	DS	SignalLimitsMinimum	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",10)	DS	SignalLimitsMaximum	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",11)	DS	SpecInfoMask	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",12)	DS	EPITimeRateOfChangeOfMagnitude	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",13)	DS	EPITimeRateOfChangeOfXComponent	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",14)	DS	EPITimeRateOfChangeOfYComponent	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",15)	DS	EPITimeRateOfChangeOfZComponent	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",16)	DS	EPITimeRateOfChangeLegalLimit1	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",17)	DS	EPIOperationModeFlag	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",18)	DS	EPIFieldCalculationSafetyFactor	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",19)	DS	EPILegalLimit1OfChangeValue	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",20)	DS	EPILegalLimit2OfChangeValue	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",21)	DS	EPIRiseTime	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",30)	DS	ArrayCoilADCOffset	16	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",31)	DS	ArrayCoilPreamplifierGain	16	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",50)	LT	SaturationType	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",51)	DS	SaturationNormalVector	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",52)	DS	SaturationPositionVector	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",53)	DS	SaturationThickness	6	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",54)	DS	SaturationWidth	6	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",55)	DS	SaturationDistance	6	PrivateTag
+
+(7fe3,"SIEMENS NUMARIS II",00)	LT	ImageGraphicsFormatCode	1	PrivateTag
+(7fe3,"SIEMENS NUMARIS II",10)	OB	ImageGraphics	1	PrivateTag
+(7fe3,"SIEMENS NUMARIS II",20)	OB	ImageGraphicsDummy	1	PrivateTag
+
+(0011,"SIEMENS RA GEN",20)	SL	FluoroTimer	1	PrivateTag
+(0011,"SIEMENS RA GEN",25)	SL	PtopDoseAreaProduct	1	PrivateTag
+(0011,"SIEMENS RA GEN",26)	SL	PtopTotalSkinDose	1	PrivateTag
+(0011,"SIEMENS RA GEN",30)	LT	Unknown	1	PrivateTag
+(0011,"SIEMENS RA GEN",35)	LO	PatientInitialPuckCounter	1	PrivateTag
+(0011,"SIEMENS RA GEN",40)	SS	SPIDataObjectType	1	PrivateTag
+(0019,"SIEMENS RA GEN",15)	LO	AcquiredPlane	1	PrivateTag
+(0019,"SIEMENS RA GEN",1f)	SS	DefaultTableIsoCenterHeight	1	PrivateTag
+(0019,"SIEMENS RA GEN",20)	SL	SceneFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",22)	SL	RefPhotofileFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",24)	LO	SceneName	1	PrivateTag
+(0019,"SIEMENS RA GEN",26)	SS	AcquisitionIndex	1	PrivateTag
+(0019,"SIEMENS RA GEN",28)	SS	MixedPulseMode	1	PrivateTag
+(0019,"SIEMENS RA GEN",2a)	SS	NoOfPositions	1	PrivateTag
+(0019,"SIEMENS RA GEN",2c)	SS	NoOfPhases	1	PrivateTag
+(0019,"SIEMENS RA GEN",2e)	SS	FrameRateForPositions	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",30)	SS	NoOfFramesForPositions	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",32)	SS	SteppingDirection	1	PrivateTag
+(0019,"SIEMENS RA GEN",34)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",36)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",38)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",3a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",3c)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",3e)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",40)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",42)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",44)	SS	ImageTransferDelay	1	PrivateTag
+(0019,"SIEMENS RA GEN",46)	SL	InversFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",48)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",4a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",4c)	SS	BlankingCircleDiameter	1	PrivateTag
+(0019,"SIEMENS RA GEN",50)	SL	StandDataValid	1	PrivateTag
+(0019,"SIEMENS RA GEN",52)	SS	TableTilt	1	PrivateTag
+(0019,"SIEMENS RA GEN",54)	SS	TableAxisRotation	1	PrivateTag
+(0019,"SIEMENS RA GEN",56)	SS	TableLongitudalPosition	1	PrivateTag
+(0019,"SIEMENS RA GEN",58)	SS	TableSideOffset	1	PrivateTag
+(0019,"SIEMENS RA GEN",5a)	SS	TableIsoCenterHeight	1	PrivateTag
+(0019,"SIEMENS RA GEN",5c)	UN	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",5e)	SL	CollimationDataValid	1	PrivateTag
+(0019,"SIEMENS RA GEN",60)	SL	PeriSequenceNo	1	PrivateTag
+(0019,"SIEMENS RA GEN",62)	SL	PeriTotalScenes	1	PrivateTag
+(0019,"SIEMENS RA GEN",64)	SL	PeriOverlapTop	1	PrivateTag
+(0019,"SIEMENS RA GEN",66)	SL	PeriOverlapBottom	1	PrivateTag
+(0019,"SIEMENS RA GEN",68)	SL	RawImageNumber	1	PrivateTag
+(0019,"SIEMENS RA GEN",6a)	SL	XRayDataValid	1	PrivateTag
+(0019,"SIEMENS RA GEN",70)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",72)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",74)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",76)	SL	FillingAverageFactor	1	PrivateTag
+(0019,"SIEMENS RA GEN",78)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",7a)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",7c)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",7e)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",80)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",82)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",84)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",86)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",88)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",8a)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",8c)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",8e)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",92)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",94)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",96)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",98)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",9a)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",9c)	SL	IntensifierLevelCalibrationFactor	1	PrivateTag
+(0019,"SIEMENS RA GEN",9e)	SL	NativeReviewFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",a2)	SL	SceneNumber	1	PrivateTag
+(0019,"SIEMENS RA GEN",a4)	SS	AcquisitionMode	1	PrivateTag
+(0019,"SIEMENS RA GEN",a5)	SS	AcquisitonFrameRate	1	PrivateTag
+(0019,"SIEMENS RA GEN",a6)	SL	ECGFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",a7)	SL	AdditionalSceneData	1	PrivateTag
+(0019,"SIEMENS RA GEN",a8)	SL	FileCopyFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",a9)	SL	PhlebovisionFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",aa)	SL	Co2Flag	1	PrivateTag
+(0019,"SIEMENS RA GEN",ab)	SS	MaxSpeed	1	PrivateTag
+(0019,"SIEMENS RA GEN",ac)	SS	StepWidth	1	PrivateTag
+(0019,"SIEMENS RA GEN",ad)	SL	DigitalAcquisitionZoom	1	PrivateTag
+(0019,"SIEMENS RA GEN",ff)	SS	Internal	1-n	PrivateTag
+(0021,"SIEMENS RA GEN",15)	SS	ImagesInStudy	1	PrivateTag
+(0021,"SIEMENS RA GEN",20)	SS	ScenesInStudy	1	PrivateTag
+(0021,"SIEMENS RA GEN",25)	SS	ImagesInPhotofile	1	PrivateTag
+(0021,"SIEMENS RA GEN",27)	SS	PlaneBImagesExist	1	PrivateTag
+(0021,"SIEMENS RA GEN",28)	SS	NoOf2MBChunks	1	PrivateTag
+(0021,"SIEMENS RA GEN",30)	SS	ImagesInAllScenes	1	PrivateTag
+(0021,"SIEMENS RA GEN",40)	SS	ArchiveSWInternalVersion	1	PrivateTag
+
+(0011,"SIEMENS RA PLANE A",28)	SL	FluoroTimerA	1	PrivateTag
+(0011,"SIEMENS RA PLANE A",29)	SL	FluoroSkinDoseA	1	PrivateTag
+(0011,"SIEMENS RA PLANE A",2a)	SL	TotalSkinDoseA	1	PrivateTag
+(0011,"SIEMENS RA PLANE A",2b)	SL	FluoroDoseAreaProductA	1	PrivateTag
+(0011,"SIEMENS RA PLANE A",2c)	SL	TotalDoseAreaProductA	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",15)	LT	OfflineUID	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",18)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",19)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",1a)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",1b)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",1c)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",1d)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",1e)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",1f)	SS	Internal	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",20)	SS	SystemCalibFactorPlaneA	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",22)	SS	XRayParameterSetNo	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",24)	SS	XRaySystem	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",26)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",28)	SS	AcquiredDisplayMode	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",2a)	SS	AcquisitionDelay	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",2c)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",2e)	SS	MaxFramesLimit	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",30)	US	MaximumFrameSizeNIU	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",32)	SS	SubtractedFilterType	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",34)	SS	FilterFactorNative	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",36)	SS	AnatomicBackgroundFactor	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",38)	SS	WindowUpperLimitNative	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",3a)	SS	WindowLowerLimitNative	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",3c)	SS	WindowBrightnessPhase1	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",3e)	SS	WindowBrightnessPhase2	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",40)	SS	WindowContrastPhase1	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",42)	SS	WindowContrastPhase2	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",44)	SS	FilterFactorSub	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",46)	SS	PeakOpacified	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",48)	SL	MaskFrame	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",4a)	SL	BIHFrame	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",4c)	SS	CentBeamAngulationCaudCran	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",4e)	SS	CentBeamAngulationLRAnterior	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",50)	SS	LongitudinalPosition	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",52)	SS	SideOffset	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",54)	SS	IsoCenterHeight	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",56)	SS	ImageTwist	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",58)	SS	SourceImageDistance	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",5a)	SS	MechanicalMagnificationFactor	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",5c)	SL	CalibrationFlag	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",5e)	SL	CalibrationAngleCranCaud	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",60)	SL	CalibrationAngleRAOLAO	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",62)	SL	CalibrationTableToFloorDist	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",64)	SL	CalibrationIsocenterToFloorDist	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",66)	SL	CalibrationIsocenterToSourceDist	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",68)	SL	CalibrationSourceToII	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",6a)	SL	CalibrationIIZoom	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",6c)	SL	CalibrationIIField	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",6e)	SL	CalibrationFactor	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",70)	SL	CalibrationObjectToImageDistance	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",72)	SL	CalibrationSystemFactor	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",74)	SL	CalibrationSystemCorrection	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",76)	SL	CalibrationSystemIIFormats	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",78)	SL	CalibrationGantryDataValid	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",7a)	SS	CollimatorSquareBreadth	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",7c)	SS	CollimatorSquareHeight	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",7e)	SS	CollimatorSquareDiameter	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",80)	SS	CollimaterFingerTurnAngle	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",82)	SS	CollimaterFingerPosition	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",84)	SS	CollimaterDiaphragmTurnAngle	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",86)	SS	CollimaterDiaphragmPosition1	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",88)	SS	CollimaterDiaphragmPosition2	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",8a)	SS	CollimaterDiaphragmMode	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",8c)	SS	CollimaterBeamLimitBreadth	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",8e)	SS	CollimaterBeamLimitHeight	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",90)	SS	CollimaterBeamLimitDiameter	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",92)	SS	X-RayControlMOde	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",94)	SS	X-RaySystem	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",96)	SS	FocalSpot	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",98)	SS	ExposureControl	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",9a)	SL	XRayVoltage	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",9c)	SL	XRayCurrent	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",9e)	SL	XRayCurrentTimeProduct	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",a0)	SL	XRayPulseTime	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",a2)	SL	XRaySceneTimeFluoroClock	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",a4)	SS	MaximumPulseRate	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",a6)	SS	PulsesPerScene	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",a8)	SL	DoseAreaProductOfScene	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",aa)	SS	Dose	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",ac)	SS	DoseRate	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",ae)	SL	IIToCoverDistance	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b0)	SS	LastFramePhase1	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b1)	SS	FrameRatePhase1	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b2)	SS	LastFramePhase2	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b3)	SS	FrameRatePhase2	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b4)	SS	LastFramePhase3	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b5)	SS	FrameRatePhase3	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b6)	SS	LastFramePhase4	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b7)	SS	FrameRatePhase4	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b8)	SS	GammaOfNativeImage	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b9)	SS	GammaOfTVSystem	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",bb)	SL	PixelshiftX	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",bc)	SL	PixelshiftY	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",bd)	SL	MaskAverageFactor	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",be)	SL	BlankingCircleFlag	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",bf)	SL	CircleRowStart	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c0)	SL	CircleRowEnd	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c1)	SL	CircleColumnStart	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c2)	SL	CircleColumnEnd	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c3)	SL	CircleDiameter	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c4)	SL	RectangularCollimaterFlag	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c5)	SL	RectangleRowStart	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c6)	SL	RectangleRowEnd	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c7)	SL	RectangleColumnStart	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c8)	SL	RectangleColumnEnd	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c9)	SL	RectangleAngulation	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",ca)	SL	IrisCollimatorFlag	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",cb)	SL	IrisRowStart	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",cc)	SL	IrisRowEnd	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",cd)	SL	IrisColumnStart	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",ce)	SL	IrisColumnEnd	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",cf)	SL	IrisAngulation	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d1)	SS	NumberOfFramesPlane	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d2)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d3)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d4)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d5)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d6)	SS	Internal	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",d7)	SS	Internal	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",d8)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d9)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",da)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",db)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",dc)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",dd)	SL	AnatomicBackground	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",de)	SL	AutoWindowBase	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",df)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",e0)	SL	Internal	1	PrivateTag
+
+(0011,"SIEMENS RA PLANE B",28)	SL	FluoroTimerB	1	PrivateTag
+(0011,"SIEMENS RA PLANE B",29)	SL	FluoroSkinDoseB	1	PrivateTag
+(0011,"SIEMENS RA PLANE B",2a)	SL	TotalSkinDoseB	1	PrivateTag
+(0011,"SIEMENS RA PLANE B",2b)	SL	FluoroDoseAreaProductB	1	PrivateTag
+(0011,"SIEMENS RA PLANE B",2c)	SL	TotalDoseAreaProductB	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",18)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",19)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",1a)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",1b)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",1c)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",1d)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",1e)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",1f)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",20)	SL	SystemCalibFactorPlaneB	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",22)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",24)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",26)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",28)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",2a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",2c)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",2e)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",30)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",32)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",34)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",36)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",38)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",3a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",3c)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",3e)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",40)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",42)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",44)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",46)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",48)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",4a)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",4c)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",4e)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",50)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",52)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",54)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",56)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",58)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",5a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",5c)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",5e)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",60)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",62)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",64)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",66)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",68)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",6a)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",6c)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",6e)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",70)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",72)	UN	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",74)	UN	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",76)	UN	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",78)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",7a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",7c)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",7e)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",80)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",82)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",84)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",86)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",88)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",8a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",8c)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",8e)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",90)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",92)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",94)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",96)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",98)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",9a)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",9c)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",9e)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",a0)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",a2)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",a4)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",a6)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",a8)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",aa)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",ac)	US	Unknown	1	PrivateTag
+
+(0011,"SIEMENS RIS",10)	LT	PatientUID	1	PrivateTag
+(0011,"SIEMENS RIS",11)	LT	PatientID	1	PrivateTag
+(0011,"SIEMENS RIS",20)	DA	PatientRegistrationDate	1	PrivateTag
+(0011,"SIEMENS RIS",21)	TM	PatientRegistrationTime	1	PrivateTag
+(0011,"SIEMENS RIS",30)	LT	PatientnameRIS	1	PrivateTag
+(0011,"SIEMENS RIS",31)	LT	PatientprenameRIS	1	PrivateTag
+(0011,"SIEMENS RIS",40)	LT	PatientHospitalStatus	1	PrivateTag
+(0011,"SIEMENS RIS",41)	LT	MedicalAlerts	1	PrivateTag
+(0011,"SIEMENS RIS",42)	LT	ContrastAllergies	1	PrivateTag
+(0031,"SIEMENS RIS",10)	LT	RequestUID	1	PrivateTag
+(0031,"SIEMENS RIS",45)	LT	RequestingPhysician	1	PrivateTag
+(0031,"SIEMENS RIS",50)	LT	RequestedPhysician	1	PrivateTag
+(0033,"SIEMENS RIS",10)	LT	PatientStudyUID	1	PrivateTag
+
+(0021,"SIEMENS SMS-AX  ACQ 1.0",00)	US	AcquisitionType	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",01)	US	AcquisitionMode	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",02)	US	FootswitchIndex	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",03)	US	AcquisitionRoom	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",04)	SL	CurrentTimeProduct	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",05)	SL	Dose	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",06)	SL	SkinDosePercent	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",07)	SL	SkinDoseAccumulation	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",08)	SL	SkinDoseRate	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",0A)	UL	CopperFilter	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",0B)	US	MeasuringField	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",0C)	SS	PostBlankingCircle	3	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",0D)	SS	DynaAngles	2-2n	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",0E)	SS	TotalSteps	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",0F)	SL	DynaXRayInfo	3-3n	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",10)	US	ModalityLUTInputGamma	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",11)	US	ModalityLUTOutputGamma	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",12)	OB	SH_STPAR	1-n	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",13)	US	AcquisitionZoom	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",14)	SS	DynaAngulationStepWidth	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",15)	US	Harmonization	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",16)	US	DRSingleFlag	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",17)	SL	SourceToIsocenter	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",18)	US	PressureData	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",19)	SL	ECGIndexArray	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",1A)	US	FDFlag	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",1B)	OB	SH_ZOOM	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",1C)	OB	SH_COLPAR	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",1D)	US	K_Factor	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",1E)	US	EVE	8	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",1F)	SL	TotalSceneTime	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",20)	US	RestoreFlag	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",21)	US	StandMovementFlag	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",22)	US	FDRows	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",23)	US	FDColumns	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",24)	US	TableMovementFlag	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",25)	LO	OriginalOrganProgramName	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",26)	DS	CrispyXPIFilter	1	PrivateTag
+
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",00)	US	ViewNative	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",01)	US	OriginalSeriesNumber	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",02)	US	OriginalImageNumber	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",03)	US	WinCenter	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",04)	US	WinWidth	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",05)	US	WinBrightness	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",06)	US	WinContrast	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",07)	US	OriginalFrameNumber	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",08)	US	OriginalMaskFrameNumber	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",09)	US	Opac	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",0A)	US	OriginalNumberOfFrames	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",0B)	DS	OriginalSceneDuration	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",0C)	LO	IdentifierLOID	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",0D)	SS	OriginalSceneVFRInfo	1-n	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",0E)	SS	OriginalFrameECGPosition	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",0F)	SS	OriginalECG1stFrameOffset_retired	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",10)	SS	ZoomFlag	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",11)	US	Flex	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",12)	US	NumberOfMaskFrames	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",13)	US	NumberOfFillFrames	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",14)	US	SeriesNumber	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",15)	IS	ImageNumber	1	PrivateTag
+
+(0023,"SIEMENS SMS-AX  QUANT 1.0",00)	DS	HorizontalCalibrationPixelSize	2	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",01)	DS	VerticalCalibrationPixelSize	2	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",02)	LO	CalibrationObject	1	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",03)	DS	CalibrationObjectSize	1	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",04)	LO	CalibrationMethod	1	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",05)	ST	Filename	1	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",06)	IS	FrameNumber	1	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",07)	IS	CalibrationFactorMultiplicity	2	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",08)	IS	CalibrationTODValue	1	PrivateTag
+
+(0019,"SIEMENS SMS-AX  VIEW 1.0",00)	US	ReviewMode	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",01)	US	AnatomicalBackgroundPercent	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",02)	US	NumberOfPhases	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",03)	US	ApplyAnatomicalBackground	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",04)	SS	PixelShiftArray	4-4n	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",05)	US	Brightness	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",06)	US	Contrast	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",07)	US	Enabled	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",08)	US	NativeEdgeEnhancementPercentGain	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",09)	SS	NativeEdgeEnhancementLUTIndex	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",0A)	SS	NativeEdgeEnhancementKernelSize	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",0B)	US	SubtrEdgeEnhancementPercentGain	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",0C)	SS	SubtrEdgeEnhancementLUTIndex	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",0D)	SS	SubtrEdgeEnhancementKernelSize	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",0E)	US	FadePercent	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",0F)	US	FlippedBeforeLateralityApplied	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",10)	US	ApplyFade	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",12)	US	Zoom	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",13)	SS	PanX	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",14)	SS	PanY	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",15)	SS	NativeEdgeEnhancementAdvPercGain	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",16)	SS	SubtrEdgeEnhancementAdvPercGain	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",17)	US	InvertFlag	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",1A)	OB	Quant1KOverlay	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",1B)	US	OriginalResolution	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",1C)	DS	AutoWindowCenter	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",1D)	DS	AutoWindowWidth	1	PrivateTag
+
+(0009,"SIENET",01)	US	SIENETCommandField	1	PrivateTag
+(0009,"SIENET",14)	LT	ReceiverPLA	1	PrivateTag
+(0009,"SIENET",16)	US	TransferPriority	1	PrivateTag
+(0009,"SIENET",29)	LT	ActualUser	1	PrivateTag
+(0095,"SIENET",01)	LT	ExaminationFolderID	1	PrivateTag
+(0095,"SIENET",04)	UL	FolderReportedStatus	1	PrivateTag
+(0095,"SIENET",05)	LT	FolderReportingRadiologist	1	PrivateTag
+(0095,"SIENET",07)	LT	SIENETISAPLA	1	PrivateTag
+(0099,"SIENET",02)	UL	DataObjectAttributes	1	PrivateTag
+
+(0009,"SPI RELEASE 1",10)	LT	Comments	1	PrivateTag
+(0009,"SPI RELEASE 1",15)	LO	SPIImageUID	1	PrivateTag
+(0009,"SPI RELEASE 1",40)	US	DataObjectType	1	PrivateTag
+(0009,"SPI RELEASE 1",41)	LO	DataObjectSubtype	1	PrivateTag
+(0011,"SPI RELEASE 1",10)	LO	Organ	1	PrivateTag
+(0011,"SPI RELEASE 1",15)	LO	AllergyIndication	1	PrivateTag
+(0011,"SPI RELEASE 1",20)	LO	Pregnancy	1	PrivateTag
+(0029,"SPI RELEASE 1",60)	LT	CompressionAlgorithm	1	PrivateTag
+
+(0009,"SPI Release 1",10)	LT	Comments	1	PrivateTag
+(0009,"SPI Release 1",15)	LO	SPIImageUID	1	PrivateTag
+(0009,"SPI Release 1",40)	US	DataObjectType	1	PrivateTag
+(0009,"SPI Release 1",41)	LO	DataObjectSubtype	1	PrivateTag
+(0011,"SPI Release 1",10)	LO	Organ	1	PrivateTag
+(0011,"SPI Release 1",15)	LO	AllergyIndication	1	PrivateTag
+(0011,"SPI Release 1",20)	LO	Pregnancy	1	PrivateTag
+(0029,"SPI Release 1",60)	LT	CompressionAlgorithm	1	PrivateTag
+
+(0009,"SPI",10)	LO	Comments	1	PrivateTag
+(0009,"SPI",15)	LO	SPIImageUID	1	PrivateTag
+(0009,"SPI",40)	US	DataObjectType	1	PrivateTag
+(0009,"SPI",41)	LT	DataObjectSubtype	1	PrivateTag
+(0011,"SPI",10)	LT	Organ	1	PrivateTag
+(0011,"SPI",15)	LT	AllergyIndication	1	PrivateTag
+(0011,"SPI",20)	LT	Pregnancy	1	PrivateTag
+(0029,"SPI",60)	LT	CompressionAlgorithm	1	PrivateTag
+
+(0011,"SPI RELEASE 1",10)	LO	Organ	1	PrivateTag
+(0011,"SPI RELEASE 1",15)	LO	AllergyIndication	1	PrivateTag
+(0011,"SPI RELEASE 1",20)	LO	Pregnancy	1	PrivateTag
+
+(0009,"SPI-P Release 1",00)	LT	DataObjectRecognitionCode	1	PrivateTag
+(0009,"SPI-P Release 1",04)	LO	ImageDataConsistence	1	PrivateTag
+(0009,"SPI-P Release 1",08)	US	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",12)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",15)	LO	UniqueIdentifier	1	PrivateTag
+(0009,"SPI-P Release 1",16)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",18)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",21)	LT	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",31)	LT	PACSUniqueIdentifier	1	PrivateTag
+(0009,"SPI-P Release 1",34)	LT	ClusterUniqueIdentifier	1	PrivateTag
+(0009,"SPI-P Release 1",38)	LT	SystemUniqueIdentifier	1	PrivateTag
+(0009,"SPI-P Release 1",39)	LT	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",51)	LT	StudyUniqueIdentifier	1	PrivateTag
+(0009,"SPI-P Release 1",61)	LT	SeriesUniqueIdentifier	1	PrivateTag
+(0009,"SPI-P Release 1",91)	LT	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",f2)	LT	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",f3)	UN	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",f4)	LT	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",f5)	UN	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",f7)	LT	Unknown	1	PrivateTag
+(0011,"SPI-P Release 1",10)	LT	PatientEntryID	1	PrivateTag
+(0011,"SPI-P Release 1",21)	UN	Unknown	1	PrivateTag
+(0011,"SPI-P Release 1",22)	UN	Unknown	1	PrivateTag
+(0011,"SPI-P Release 1",31)	UN	Unknown	1	PrivateTag
+(0011,"SPI-P Release 1",32)	UN	Unknown	1	PrivateTag
+(0019,"SPI-P Release 1",00)	UN	Unknown	1	PrivateTag
+(0019,"SPI-P Release 1",01)	UN	Unknown	1	PrivateTag
+(0019,"SPI-P Release 1",02)	UN	Unknown	1	PrivateTag
+(0019,"SPI-P Release 1",10)	US	MainsFrequency	1	PrivateTag
+(0019,"SPI-P Release 1",25)	LT	OriginalPixelDataQuality	1-n	PrivateTag
+(0019,"SPI-P Release 1",30)	US	ECGTriggering	1	PrivateTag
+(0019,"SPI-P Release 1",31)	UN	ECG1Offset	1	PrivateTag
+(0019,"SPI-P Release 1",32)	UN	ECG2Offset1	1	PrivateTag
+(0019,"SPI-P Release 1",33)	UN	ECG2Offset2	1	PrivateTag
+(0019,"SPI-P Release 1",50)	US	VideoScanMode	1	PrivateTag
+(0019,"SPI-P Release 1",51)	US	VideoLineRate	1	PrivateTag
+(0019,"SPI-P Release 1",60)	US	XrayTechnique	1	PrivateTag
+(0019,"SPI-P Release 1",61)	DS	ImageIdentifierFromat	1	PrivateTag
+(0019,"SPI-P Release 1",62)	US	IrisDiaphragm	1	PrivateTag
+(0019,"SPI-P Release 1",63)	CS	Filter	1	PrivateTag
+(0019,"SPI-P Release 1",64)	CS	CineParallel	1	PrivateTag
+(0019,"SPI-P Release 1",65)	CS	CineMaster	1	PrivateTag
+(0019,"SPI-P Release 1",70)	US	ExposureChannel	1	PrivateTag
+(0019,"SPI-P Release 1",71)	UN	ExposureChannelFirstImage	1	PrivateTag
+(0019,"SPI-P Release 1",72)	US	ProcessingChannel	1	PrivateTag
+(0019,"SPI-P Release 1",80)	DS	AcquisitionDelay	1	PrivateTag
+(0019,"SPI-P Release 1",81)	UN	RelativeImageTime	1	PrivateTag
+(0019,"SPI-P Release 1",90)	CS	VideoWhiteCompression	1	PrivateTag
+(0019,"SPI-P Release 1",a0)	US	Angulation	1	PrivateTag
+(0019,"SPI-P Release 1",a1)	US	Rotation	1	PrivateTag
+(0021,"SPI-P Release 1",12)	LT	SeriesUniqueIdentifier	1	PrivateTag
+(0021,"SPI-P Release 1",14)	LT	Unknown	1	PrivateTag
+(0029,"SPI-P Release 1",00)	DS	Unknown	4	PrivateTag
+(0029,"SPI-P Release 1",20)	DS	PixelAspectRatio	1	PrivateTag
+(0029,"SPI-P Release 1",25)	LO	ProcessedPixelDataQuality	1-n	PrivateTag
+(0029,"SPI-P Release 1",30)	LT	Unknown	1	PrivateTag
+(0029,"SPI-P Release 1",38)	US	Unknown	1	PrivateTag
+(0029,"SPI-P Release 1",60)	LT	Unknown	1	PrivateTag
+(0029,"SPI-P Release 1",61)	LT	Unknown	1	PrivateTag
+(0029,"SPI-P Release 1",67)	LT	Unknown	1	PrivateTag
+(0029,"SPI-P Release 1",70)	LT	WindowID	1	PrivateTag
+(0029,"SPI-P Release 1",71)	CS	VideoInvertSubtracted	1	PrivateTag
+(0029,"SPI-P Release 1",72)	CS	VideoInvertNonsubtracted	1	PrivateTag
+(0029,"SPI-P Release 1",77)	CS	WindowSelectStatus	1	PrivateTag
+(0029,"SPI-P Release 1",78)	LT	ECGDisplayPrintingID	1	PrivateTag
+(0029,"SPI-P Release 1",79)	CS	ECGDisplayPrinting	1	PrivateTag
+(0029,"SPI-P Release 1",7e)	CS	ECGDisplayPrintingEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1",7f)	CS	ECGDisplayPrintingSelectStatus	1	PrivateTag
+(0029,"SPI-P Release 1",80)	LT	PhysiologicalDisplayID	1	PrivateTag
+(0029,"SPI-P Release 1",81)	US	PreferredPhysiologicalChannelDisplay	1	PrivateTag
+(0029,"SPI-P Release 1",8e)	CS	PhysiologicalDisplayEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1",8f)	CS	PhysiologicalDisplaySelectStatus	1	PrivateTag
+(0029,"SPI-P Release 1",c0)	LT	FunctionalShutterID	1	PrivateTag
+(0029,"SPI-P Release 1",c1)	US	FieldOfShutter	1	PrivateTag
+(0029,"SPI-P Release 1",c5)	LT	FieldOfShutterRectangle	1	PrivateTag
+(0029,"SPI-P Release 1",ce)	CS	ShutterEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1",cf)	CS	ShutterSelectStatus	1	PrivateTag
+(7FE1,"SPI-P Release 1",10)	ox	PixelData	1	PrivateTag
+
+(0009,"SPI-P Release 1;1",c0)	LT	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1;1",c1)	LT	Unknown	1	PrivateTag
+(0019,"SPI-P Release 1;1",00)	UN	PhysiologicalDataType	1	PrivateTag
+(0019,"SPI-P Release 1;1",01)	UN	PhysiologicalDataChannelAndKind	1	PrivateTag
+(0019,"SPI-P Release 1;1",02)	US	SampleBitsAllocated	1	PrivateTag
+(0019,"SPI-P Release 1;1",03)	US	SampleBitsStored	1	PrivateTag
+(0019,"SPI-P Release 1;1",04)	US	SampleHighBit	1	PrivateTag
+(0019,"SPI-P Release 1;1",05)	US	SampleRepresentation	1	PrivateTag
+(0019,"SPI-P Release 1;1",06)	UN	SmallestSampleValue	1	PrivateTag
+(0019,"SPI-P Release 1;1",07)	UN	LargestSampleValue	1	PrivateTag
+(0019,"SPI-P Release 1;1",08)	UN	NumberOfSamples	1	PrivateTag
+(0019,"SPI-P Release 1;1",09)	UN	SampleData	1	PrivateTag
+(0019,"SPI-P Release 1;1",0a)	UN	SampleRate	1	PrivateTag
+(0019,"SPI-P Release 1;1",10)	UN	PhysiologicalDataType2	1	PrivateTag
+(0019,"SPI-P Release 1;1",11)	UN	PhysiologicalDataChannelAndKind2	1	PrivateTag
+(0019,"SPI-P Release 1;1",12)	US	SampleBitsAllocated2	1	PrivateTag
+(0019,"SPI-P Release 1;1",13)	US	SampleBitsStored2	1	PrivateTag
+(0019,"SPI-P Release 1;1",14)	US	SampleHighBit2	1	PrivateTag
+(0019,"SPI-P Release 1;1",15)	US	SampleRepresentation2	1	PrivateTag
+(0019,"SPI-P Release 1;1",16)	UN	SmallestSampleValue2	1	PrivateTag
+(0019,"SPI-P Release 1;1",17)	UN	LargestSampleValue2	1	PrivateTag
+(0019,"SPI-P Release 1;1",18)	UN	NumberOfSamples2	1	PrivateTag
+(0019,"SPI-P Release 1;1",19)	UN	SampleData2	1	PrivateTag
+(0019,"SPI-P Release 1;1",1a)	UN	SampleRate2	1	PrivateTag
+(0029,"SPI-P Release 1;1",00)	LT	ZoomID	1	PrivateTag
+(0029,"SPI-P Release 1;1",01)	DS	ZoomRectangle	1-n	PrivateTag
+(0029,"SPI-P Release 1;1",03)	DS	ZoomFactor	1	PrivateTag
+(0029,"SPI-P Release 1;1",04)	US	ZoomFunction	1	PrivateTag
+(0029,"SPI-P Release 1;1",0e)	CS	ZoomEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1;1",0f)	CS	ZoomSelectStatus	1	PrivateTag
+(0029,"SPI-P Release 1;1",40)	LT	MagnifyingGlassID	1	PrivateTag
+(0029,"SPI-P Release 1;1",41)	DS	MagnifyingGlassRectangle	1-n	PrivateTag
+(0029,"SPI-P Release 1;1",43)	DS	MagnifyingGlassFactor	1	PrivateTag
+(0029,"SPI-P Release 1;1",44)	US	MagnifyingGlassFunction	1	PrivateTag
+(0029,"SPI-P Release 1;1",4e)	CS	MagnifyingGlassEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1;1",4f)	CS	MagnifyingGlassSelectStatus	1	PrivateTag
+
+(0029,"SPI-P Release 1;2",00)	LT	SubtractionMaskID	1	PrivateTag
+(0029,"SPI-P Release 1;2",04)	UN	MaskingFunction	1	PrivateTag
+(0029,"SPI-P Release 1;2",0c)	UN	ProprietaryMaskingParameters	1	PrivateTag
+(0029,"SPI-P Release 1;2",1e)	CS	SubtractionMaskEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1;2",1f)	CS	SubtractionMaskSelectStatus	1	PrivateTag
+(0029,"SPI-P Release 1;3",00)	LT	ImageEnhancementID	1	PrivateTag
+(0029,"SPI-P Release 1;3",01)	LT	ImageEnhancement	1	PrivateTag
+(0029,"SPI-P Release 1;3",02)	LT	ConvolutionID	1	PrivateTag
+(0029,"SPI-P Release 1;3",03)	LT	ConvolutionType	1	PrivateTag
+(0029,"SPI-P Release 1;3",04)	LT	ConvolutionKernelSizeID	1	PrivateTag
+(0029,"SPI-P Release 1;3",05)	US	ConvolutionKernelSize	2	PrivateTag
+(0029,"SPI-P Release 1;3",06)	US	ConvolutionKernel	1-n	PrivateTag
+(0029,"SPI-P Release 1;3",0c)	DS	EnhancementGain	1	PrivateTag
+(0029,"SPI-P Release 1;3",1e)	CS	ImageEnhancementEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1;3",1f)	CS	ImageEnhancementSelectStatus	1	PrivateTag
+
+(0011,"SPI-P Release 2;1",18)	LT	Unknown	1	PrivateTag
+(0023,"SPI-P Release 2;1",0d)	UI	Unknown	1	PrivateTag
+(0023,"SPI-P Release 2;1",0e)	UI	Unknown	1	PrivateTag
+
+(0009,"SPI-P-GV-CT Release 1",00)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",10)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",20)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",30)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",40)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",50)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",60)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",70)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",75)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",80)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",90)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",08)	IS	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",09)	IS	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",0a)	IS	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",10)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",20)	TM	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",50)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",60)	DS	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",61)	US	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",63)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",64)	US	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",65)	IS	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",70)	LT	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",80)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",81)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",90)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",a0)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",a1)	US	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",a2)	US	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",a3)	US	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",b0)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",b1)	LO	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",20)	LO	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",30)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",40)	LO	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",50)	LO	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",60)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",70)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",80)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",90)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",a0)	US	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",a1)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",a2)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",a3)	LT	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",a4)	LT	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",b0)	LO	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",c0)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",10)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",30)	UL	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",31)	UL	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",32)	UL	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",33)	UL	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",80)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",90)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",d0)	IS	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",d1)	IS	Unknown	1	PrivateTag
+
+(0019,"SPI-P-PCR Release 2",30)	US	Unknown	1	PrivateTag
+
+(0021,"SPI-P-Private-CWS Release 1",00)	LT	WindowOfImagesID	1	PrivateTag
+(0021,"SPI-P-Private-CWS Release 1",01)	CS	WindowOfImagesType	1	PrivateTag
+(0021,"SPI-P-Private-CWS Release 1",02)	IS	WindowOfImagesScope	1-n	PrivateTag
+
+(0019,"SPI-P-Private-DCI Release 1",10)	UN	ECGTimeMapDataBitsAllocated	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",11)	UN	ECGTimeMapDataBitsStored	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",12)	UN	ECGTimeMapDataHighBit	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",13)	UN	ECGTimeMapDataRepresentation	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",14)	UN	ECGTimeMapDataSmallestDataValue	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",15)	UN	ECGTimeMapDataLargestDataValue	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",16)	UN	ECGTimeMapDataNumberOfDataValues	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",17)	UN	ECGTimeMapData	1	PrivateTag
+
+(0021,"SPI-P-Private_CDS Release 1",40)	IS	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_CDS Release 1",00)	UN	Unknown	1	PrivateTag
+
+(0019,"SPI-P-Private_ICS Release 1",30)	DS	Unknown	1	PrivateTag
+(0019,"SPI-P-Private_ICS Release 1",31)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",08)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",0f)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",10)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",1b)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",1c)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",21)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",43)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",44)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",4C)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",67)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",68)	US	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",6A)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",6B)	US	Unknown	1	PrivateTag
+
+(0029,"SPI-P-Private_ICS Release 1;1",00)	SL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;1",05)	FL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;1",06)	FL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;1",20)	FL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;1",21)	FL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;1",CD)	SQ	Unknown	1	PrivateTag
+
+(0029,"SPI-P-Private_ICS Release 1;2",00)	FD	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;2",01)	FD	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;2",02)	FD	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;2",03)	SL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;2",04)	SL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;2",05)	SL	Unknown	1	PrivateTag
+
+(0029,"SPI-P-Private_ICS Release 1;3",C0)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;3",C1)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;3",C2)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;3",C3)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;3",C4)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;3",C5)	SQ	Unknown	1	PrivateTag
+
+(0029,"SPI-P-Private_ICS Release 1;4",02)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;4",9A)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;4",E0)	SQ	Unknown	1	PrivateTag
+
+(0029,"SPI-P-Private_ICS Release 1;5",50)	CS	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;5",55)	CS	Unknown	1	PrivateTag
+
+(0019,"SPI-P-XSB-DCI Release 1",10)	LT	VideoBeamBoost	1	PrivateTag
+(0019,"SPI-P-XSB-DCI Release 1",11)	US	ChannelGeneratingVideoSync	1	PrivateTag
+(0019,"SPI-P-XSB-DCI Release 1",12)	US	VideoGain	1	PrivateTag
+(0019,"SPI-P-XSB-DCI Release 1",13)	US	VideoOffset	1	PrivateTag
+(0019,"SPI-P-XSB-DCI Release 1",20)	DS	RTDDataCompressionFactor	1	PrivateTag
+
+(0029,"Silhouette Annot V1.0",11)	IS	AnnotationName	1	PrivateTag
+(0029,"Silhouette Annot V1.0",12)	LT	AnnotationFont	1	PrivateTag
+(0029,"Silhouette Annot V1.0",13)	LT	AnnotationTextForegroundColor	1	PrivateTag
+(0029,"Silhouette Annot V1.0",14)	LT	AnnotationTextBackgroundColor	1	PrivateTag
+(0029,"Silhouette Annot V1.0",15)	UL	AnnotationTextBackingMode	1	PrivateTag
+(0029,"Silhouette Annot V1.0",16)	UL	AnnotationTextJustification	1	PrivateTag
+(0029,"Silhouette Annot V1.0",17)	UL	AnnotationTextLocation	1	PrivateTag
+(0029,"Silhouette Annot V1.0",18)	LT	AnnotationTextString	1	PrivateTag
+(0029,"Silhouette Annot V1.0",19)	UL	AnnotationTextAttachMode	1	PrivateTag
+(0029,"Silhouette Annot V1.0",20)	UL	AnnotationTextCursorMode	1	PrivateTag
+(0029,"Silhouette Annot V1.0",21)	UL	AnnotationTextShadowOffsetX	1	PrivateTag
+(0029,"Silhouette Annot V1.0",22)	UL	AnnotationTextShadowOffsetY	1	PrivateTag
+(0029,"Silhouette Annot V1.0",23)	LT	AnnotationLineColor	1	PrivateTag
+(0029,"Silhouette Annot V1.0",24)	UL	AnnotationLineThickness	1	PrivateTag
+(0029,"Silhouette Annot V1.0",25)	UL	AnnotationLineType	1	PrivateTag
+(0029,"Silhouette Annot V1.0",26)	UL	AnnotationLineStyle	1	PrivateTag
+(0029,"Silhouette Annot V1.0",27)	UL	AnnotationLineDashLength	1	PrivateTag
+(0029,"Silhouette Annot V1.0",28)	UL	AnnotationLineAttachMode	1	PrivateTag
+(0029,"Silhouette Annot V1.0",29)	UL	AnnotationLinePointCount	1	PrivateTag
+(0029,"Silhouette Annot V1.0",30)	FD	AnnotationLinePoints	1	PrivateTag
+(0029,"Silhouette Annot V1.0",31)	UL	AnnotationLineControlSize	1	PrivateTag
+(0029,"Silhouette Annot V1.0",32)	LT	AnnotationMarkerColor	1	PrivateTag
+(0029,"Silhouette Annot V1.0",33)	UL	AnnotationMarkerType	1	PrivateTag
+(0029,"Silhouette Annot V1.0",34)	UL	AnnotationMarkerSize	1	PrivateTag
+(0029,"Silhouette Annot V1.0",35)	FD	AnnotationMarkerLocation	1	PrivateTag
+(0029,"Silhouette Annot V1.0",36)	UL	AnnotationMarkerAttachMode	1	PrivateTag
+(0029,"Silhouette Annot V1.0",37)	LT	AnnotationGeomColor	1	PrivateTag
+(0029,"Silhouette Annot V1.0",38)	UL	AnnotationGeomThickness	1	PrivateTag
+(0029,"Silhouette Annot V1.0",39)	UL	AnnotationGeomLineStyle	1	PrivateTag
+(0029,"Silhouette Annot V1.0",40)	UL	AnnotationGeomDashLength	1	PrivateTag
+(0029,"Silhouette Annot V1.0",41)	UL	AnnotationGeomFillPattern	1	PrivateTag
+(0029,"Silhouette Annot V1.0",42)	UL	AnnotationInteractivity	1	PrivateTag
+(0029,"Silhouette Annot V1.0",43)	FD	AnnotationArrowLength	1	PrivateTag
+(0029,"Silhouette Annot V1.0",44)	FD	AnnotationArrowAngle	1	PrivateTag
+(0029,"Silhouette Annot V1.0",45)	UL	AnnotationDontSave	1	PrivateTag
+
+(0029,"Silhouette Graphics Export V1.0",00)	UI	Unknown	1	PrivateTag
+
+(0029,"Silhouette Line V1.0",11)	IS	LineName	1	PrivateTag
+(0029,"Silhouette Line V1.0",12)	LT	LineNameFont	1	PrivateTag
+(0029,"Silhouette Line V1.0",13)	UL	LineNameDisplay	1	PrivateTag
+(0029,"Silhouette Line V1.0",14)	LT	LineNormalColor	1	PrivateTag
+(0029,"Silhouette Line V1.0",15)	UL	LineType	1	PrivateTag
+(0029,"Silhouette Line V1.0",16)	UL	LineThickness	1	PrivateTag
+(0029,"Silhouette Line V1.0",17)	UL	LineStyle	1	PrivateTag
+(0029,"Silhouette Line V1.0",18)	UL	LineDashLength	1	PrivateTag
+(0029,"Silhouette Line V1.0",19)	UL	LineInteractivity	1	PrivateTag
+(0029,"Silhouette Line V1.0",20)	LT	LineMeasurementColor	1	PrivateTag
+(0029,"Silhouette Line V1.0",21)	LT	LineMeasurementFont	1	PrivateTag
+(0029,"Silhouette Line V1.0",22)	UL	LineMeasurementDashLength	1	PrivateTag
+(0029,"Silhouette Line V1.0",23)	UL	LinePointSpace	1	PrivateTag
+(0029,"Silhouette Line V1.0",24)	FD	LinePoints	1	PrivateTag
+(0029,"Silhouette Line V1.0",25)	UL	LineControlPointSize	1	PrivateTag
+(0029,"Silhouette Line V1.0",26)	UL	LineControlPointSpace	1	PrivateTag
+(0029,"Silhouette Line V1.0",27)	FD	LineControlPoints	1	PrivateTag
+(0029,"Silhouette Line V1.0",28)	LT	LineLabel	1	PrivateTag
+(0029,"Silhouette Line V1.0",29)	UL	LineDontSave	1	PrivateTag
+
+(0029,"Silhouette ROI V1.0",11)	IS	ROIName	1	PrivateTag
+(0029,"Silhouette ROI V1.0",12)	LT	ROINameFont	1	PrivateTag
+(0029,"Silhouette ROI V1.0",13)	LT	ROINormalColor	1	PrivateTag
+(0029,"Silhouette ROI V1.0",14)	UL	ROIFillPattern	1	PrivateTag
+(0029,"Silhouette ROI V1.0",15)	UL	ROIBpSeg	1	PrivateTag
+(0029,"Silhouette ROI V1.0",16)	UN	ROIBpSegPairs	1	PrivateTag
+(0029,"Silhouette ROI V1.0",17)	UL	ROISeedSpace	1	PrivateTag
+(0029,"Silhouette ROI V1.0",18)	UN	ROISeeds	1	PrivateTag
+(0029,"Silhouette ROI V1.0",19)	UL	ROILineThickness	1	PrivateTag
+(0029,"Silhouette ROI V1.0",20)	UL	ROILineStyle	1	PrivateTag
+(0029,"Silhouette ROI V1.0",21)	UL	ROILineDashLength	1	PrivateTag
+(0029,"Silhouette ROI V1.0",22)	UL	ROIInteractivity	1	PrivateTag
+(0029,"Silhouette ROI V1.0",23)	UL	ROINamePosition	1	PrivateTag
+(0029,"Silhouette ROI V1.0",24)	UL	ROINameDisplay	1	PrivateTag
+(0029,"Silhouette ROI V1.0",25)	LT	ROILabel	1	PrivateTag
+(0029,"Silhouette ROI V1.0",26)	UL	ROIShape	1	PrivateTag
+(0029,"Silhouette ROI V1.0",27)	FD	ROIShapeTilt	1	PrivateTag
+(0029,"Silhouette ROI V1.0",28)	UL	ROIShapePointsCount	1	PrivateTag
+(0029,"Silhouette ROI V1.0",29)	UL	ROIShapePointsSpace	1	PrivateTag
+(0029,"Silhouette ROI V1.0",30)	FD	ROIShapePoints	1	PrivateTag
+(0029,"Silhouette ROI V1.0",31)	UL	ROIShapeControlPointsCount	1	PrivateTag
+(0029,"Silhouette ROI V1.0",32)	UL	ROIShapeControlPointsSpace	1	PrivateTag
+(0029,"Silhouette ROI V1.0",33)	FD	ROIShapeControlPoints	1	PrivateTag
+(0029,"Silhouette ROI V1.0",34)	UL	ROIDontSave	1	PrivateTag
+
+(0029,"Silhouette Sequence Ids V1.0",41)	SQ	Unknown	1	PrivateTag
+(0029,"Silhouette Sequence Ids V1.0",42)	SQ	Unknown	1	PrivateTag
+(0029,"Silhouette Sequence Ids V1.0",43)	SQ	Unknown	1	PrivateTag
+
+(0029,"Silhouette V1.0",13)	UL	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",14)	UL	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",17)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",18)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",19)	UL	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",1a)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",1b)	UL	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",1c)	UL	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",1d)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",1e)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",21)	US	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",22)	US	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",23)	US	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",24)	US	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",25)	US	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",27)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",28)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",29)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",30)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",52)	US	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",53)	LT	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",54)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",55)	LT	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",56)	LT	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",57)	UN	Unknown	1	PrivateTag
+
+(0135,"SONOWAND AS",10)	LO	UltrasoundScannerName	1	PrivateTag
+(0135,"SONOWAND AS",11)	LO	TransducerSerial	1	PrivateTag
+(0135,"SONOWAND AS",12)	LO	ProbeApplication	1	PrivateTag
+
+(0017,"SVISION",00)	LO	ExtendedBodyPart	1	PrivateTag
+(0017,"SVISION",10)	LO	ExtendedViewPosition	1	PrivateTag
+(0017,"SVISION",F0)	IS	ImagesSOPClass	1	PrivateTag
+(0019,"SVISION",00)	IS	AECField	1	PrivateTag
+(0019,"SVISION",01)	IS	AECFilmScreen	1	PrivateTag
+(0019,"SVISION",02)	IS	AECDensity	1	PrivateTag
+(0019,"SVISION",10)	IS	PatientThickness	1	PrivateTag
+(0019,"SVISION",18)	IS	BeamDistance	1	PrivateTag
+(0019,"SVISION",20)	IS	WorkstationNumber	1	PrivateTag
+(0019,"SVISION",28)	IS	TubeNumber	1	PrivateTag
+(0019,"SVISION",30)	IS	BuckyGrid	1	PrivateTag
+(0019,"SVISION",34)	IS	Focus	1	PrivateTag
+(0019,"SVISION",38)	IS	Child	1	PrivateTag
+(0019,"SVISION",40)	IS	CollimatorDistanceX	1	PrivateTag
+(0019,"SVISION",41)	IS	CollimatorDistanceY	1	PrivateTag
+(0019,"SVISION",50)	IS	CentralBeamHeight	1	PrivateTag
+(0019,"SVISION",60)	IS	BuckyAngle	1	PrivateTag
+(0019,"SVISION",68)	IS	CArmAngle	1	PrivateTag
+(0019,"SVISION",69)	IS	CollimatorAngle	1	PrivateTag
+(0019,"SVISION",70)	IS	FilterNumber	1	PrivateTag
+(0019,"SVISION",74)	LO	FilterMaterial1	1	PrivateTag
+(0019,"SVISION",75)	LO	FilterMaterial2	1	PrivateTag
+(0019,"SVISION",78)	DS	FilterThickness1	1	PrivateTag
+(0019,"SVISION",79)	DS	FilterThickness2	1	PrivateTag
+(0019,"SVISION",80)	IS	BuckyFormat	1	PrivateTag
+(0019,"SVISION",81)	IS	ObjectPosition	1	PrivateTag
+(0019,"SVISION",90)	LO	DeskCommand	1	PrivateTag
+(0019,"SVISION",A0)	DS	ExtendedExposureTime	1	PrivateTag
+(0019,"SVISION",A1)	DS	ActualExposureTime	1	PrivateTag
+(0019,"SVISION",A8)	DS	ExtendedXRayTubeCurrent	1	PrivateTag
+(0021,"SVISION",00)	DS	NoiseReduction	1	PrivateTag
+(0021,"SVISION",01)	DS	ContrastAmplification	1	PrivateTag
+(0021,"SVISION",02)	DS	EdgeContrastBoosting	1	PrivateTag
+(0021,"SVISION",03)	DS	LatitudeReduction	1	PrivateTag
+(0021,"SVISION",10)	LO	FindRangeAlgorithm	1	PrivateTag
+(0021,"SVISION",11)	DS	ThresholdCAlgorithm	1	PrivateTag
+(0021,"SVISION",20)	LO	SensometricCurve	1	PrivateTag
+(0021,"SVISION",30)	DS	LowerWindowOffset	1	PrivateTag
+(0021,"SVISION",31)	DS	UpperWindowOffset	1	PrivateTag
+(0021,"SVISION",40)	DS	MinPrintableDensity	1	PrivateTag
+(0021,"SVISION",41)	DS	MaxPrintableDensity	1	PrivateTag
+(0021,"SVISION",90)	DS	Brightness	1	PrivateTag
+(0021,"SVISION",91)	DS	Contrast	1	PrivateTag
+(0021,"SVISION",92)	DS	ShapeFactor	1	PrivateTag
+(0023,"SVISION",00)	LO	ImageLaterality	1	PrivateTag
+(0023,"SVISION",01)	IS	LetterPosition	1	PrivateTag
+(0023,"SVISION",02)	IS	BurnedInAnnotation	1	PrivateTag
+(0023,"SVISION",03)	LO	Unknown	1	PrivateTag
+(0023,"SVISION",F0)	IS	ImageSOPClass	1	PrivateTag
+(0025,"SVISION",00)	IS	OriginalImage	1	PrivateTag
+(0025,"SVISION",01)	IS	NotProcessedImage	1	PrivateTag
+(0025,"SVISION",02)	IS	CutOutImage	1	PrivateTag
+(0025,"SVISION",03)	IS	DuplicatedImage	1	PrivateTag
+(0025,"SVISION",04)	IS	StoredImage	1	PrivateTag
+(0025,"SVISION",05)	IS	RetrievedImage	1	PrivateTag
+(0025,"SVISION",06)	IS	RemoteImage	1	PrivateTag
+(0025,"SVISION",07)	IS	MediaStoredImage	1	PrivateTag
+(0025,"SVISION",08)	IS	ImageState	1	PrivateTag
+(0025,"SVISION",20)	LO	SourceImageFile	1	PrivateTag
+(0025,"SVISION",21)	UI	Unknown	1	PrivateTag
+(0027,"SVISION",00)	IS	NumberOfSeries	1	PrivateTag
+(0027,"SVISION",01)	IS	NumberOfStudies	1	PrivateTag
+(0027,"SVISION",10)	DT	OldestSeries	1	PrivateTag
+(0027,"SVISION",11)	DT	NewestSeries	1	PrivateTag
+(0027,"SVISION",12)	DT	OldestStudy	1	PrivateTag
+(0027,"SVISION",13)	DT	NewestStudy	1	PrivateTag
+
+(0009,"TOSHIBA_MEC_1.0",01)	LT	Unknown	1	PrivateTag
+(0009,"TOSHIBA_MEC_1.0",02)	US	Unknown	1-n	PrivateTag
+(0009,"TOSHIBA_MEC_1.0",03)	US	Unknown	1-n	PrivateTag
+(0009,"TOSHIBA_MEC_1.0",04)	US	Unknown	1-n	PrivateTag
+(0011,"TOSHIBA_MEC_1.0",01)	LT	Unknown	1	PrivateTag
+(0011,"TOSHIBA_MEC_1.0",02)	US	Unknown	1-n	PrivateTag
+(0019,"TOSHIBA_MEC_1.0",01)	US	Unknown	1-n	PrivateTag
+(0019,"TOSHIBA_MEC_1.0",02)	US	Unknown	1-n	PrivateTag
+(0021,"TOSHIBA_MEC_1.0",01)	US	Unknown	1-n	PrivateTag
+(0021,"TOSHIBA_MEC_1.0",02)	US	Unknown	1-n	PrivateTag
+(0021,"TOSHIBA_MEC_1.0",03)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_1.0",01)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_1.0",02)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_1.0",03)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_1.0",10)	US	Unknown	1-n	PrivateTag
+
+(0019,"TOSHIBA_MEC_CT_1.0",01)	IS	Unknown	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",02)	IS	Unknown	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",03)	US	Unknown	1-n	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",04)	LT	Unknown	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",05)	LT	Unknown	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",06)	US	Unknown	1-n	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",07)	US	Unknown	1-n	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",08)	LT	OrientationHeadFeet	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",09)	LT	ViewDirection	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",0a)	LT	OrientationSupineProne	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",0b)	DS	Unknown	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",0c)	US	Unknown	1-n	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",0d)	TM	Time	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",0e)	DS	Unknown	1	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",01)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",02)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",03)	IS	Unknown	1	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",04)	IS	Unknown	1	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",05)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",07)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",08)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",09)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",0a)	LT	Unknown	1	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",0b)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",0c)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",0d)	US	Unknown	1-n	PrivateTag
+#
+# end of private.dic
+#
--- a/Resources/Patches/dcmtk-linux-speed.patch	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-diff -urEb dcmtk-3.6.0.orig/dcmnet/libsrc/dul.cc dcmtk-3.6.0/dcmnet/libsrc/dul.cc
---- dcmtk-3.6.0.orig/dcmnet/libsrc/dul.cc	2010-12-01 09:26:36.000000000 +0100
-+++ dcmtk-3.6.0/dcmnet/libsrc/dul.cc	2015-05-15 17:03:50.762451757 +0200
-@@ -1840,7 +1840,7 @@
-     }
- #endif
- #endif
--    setTCPBufferLength(sock);
-+    //setTCPBufferLength(sock);
- 
- #ifndef DONT_DISABLE_NAGLE_ALGORITHM
-     /*
-diff -urEb dcmtk-3.6.0.orig/dcmnet/libsrc/dulfsm.cc dcmtk-3.6.0/dcmnet/libsrc/dulfsm.cc
---- dcmtk-3.6.0.orig/dcmnet/libsrc/dulfsm.cc	2010-12-01 09:26:36.000000000 +0100
-+++ dcmtk-3.6.0/dcmnet/libsrc/dulfsm.cc	2015-05-15 17:03:55.570451952 +0200
-@@ -2417,7 +2417,7 @@
-           return makeDcmnetCondition(DULC_TCPINITERROR, OF_error, msg.c_str());
-         }
- #endif
--        setTCPBufferLength(s);
-+        //setTCPBufferLength(s);
- 
- #ifndef DONT_DISABLE_NAGLE_ALGORITHM
-         /*
--- a/Resources/Patches/dcmtk-mingw64.patch	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-diff -urEb dcmtk-3.6.0.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.0/ofstd/include/dcmtk/ofstd/offile.h
---- dcmtk-3.6.0.orig/ofstd/include/dcmtk/ofstd/offile.h	2010-12-17 11:50:30.000000000 +0100
-+++ dcmtk-3.6.0/ofstd/include/dcmtk/ofstd/offile.h	2013-07-19 15:56:25.688996134 +0200
-@@ -196,7 +196,7 @@
-   OFBool popen(const char *command, const char *modes)
-   {
-     if (file_) fclose();
--#ifdef _WIN32
-+#if 0
-     file_ = _popen(command, modes);
- #else
-     file_ = :: popen(command, modes);
-@@ -258,7 +258,7 @@
-     {
-       if (popened_)
-       {
--#ifdef _WIN32
-+#if 0
-         result = _pclose(file_);
- #else
-         result = :: pclose(file_);
-Only in dcmtk-3.6.0/ofstd/include/dcmtk/ofstd: offile.h~
--- a/Resources/Patches/dcmtk-mingw64.txt	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-diff -urEb dcmtk-3.6.0.orig/ dcmtk-3.6.0 > ../Resources/Patches/dcmtk-mingw64.patch
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/dcmtk.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,10 @@
+Generate some patch
+===================
+
+diff -urEb dcmtk-3.6.0.orig/ dcmtk-3.6.0
+diff -urEb dcmtk-3.6.2.orig/ dcmtk-3.6.2
+
+For "dcmtk-3.6.2-private.dic"
+=============================
+
+# cp ../../ThirdPartyDownloads/dcmtk-3.6.2/dcmdata/data/private.dic dcmtk-3.6.2-private.dic
--- a/Resources/Patches/glog-port-cc.diff	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
---- i/glog-0.3.2/src/windows/port.cc	2011-08-30 09:56:13.000000000 +0200
-+++ port.cc	2012-10-03 17:11:54.000000000 +0200
-@@ -55,6 +55,7 @@
-   return _vsnprintf(str, size-1, format, ap);
- }
- 
-+#if !defined(__MINGW32__)
- int snprintf(char *str, size_t size, const char *format, ...) {
-   va_list ap;
-   va_start(ap, format);
-@@ -62,3 +63,4 @@
-   va_end(ap);
-   return r;
- }
-+#endif
--- a/Resources/Patches/glog-port-h-v2.diff	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-124,130c124,146
-< // ----------------------------------- THREADS
-< typedef DWORD pthread_t;
-< typedef DWORD pthread_key_t;
-< typedef LONG pthread_once_t;
-< enum { PTHREAD_ONCE_INIT = 0 };   // important that this be 0! for SpinLock
-< #define pthread_self  GetCurrentThreadId
-< #define pthread_equal(pthread_t_1, pthread_t_2)  ((pthread_t_1)==(pthread_t_2))
----
-> // ----------------------------------- SECURE STRINGS
-> 
-> #if HAVE_SECURE_STRING_EXTENSIONS == 0
-> // Emulation of "localtime_s" and "strerror_s" for old versions of MinGW
-> inline int localtime_s(tm * _tm, const time_t * time)
-> {
->   tm * posix_local_time_struct = localtime(time);
->   if (posix_local_time_struct == NULL) 
->   {
->     return 1;
->   }
-> 
->   *_tm = *posix_local_time_struct;
-> 
->   return 0;
-> }
-> 
-> inline char* strerror_s(char* buf, size_t buflen, int errnum) 
-> {
->   const char* str = strerror(errnum);
->   return strncpy(buf, str, buflen - 1);
-> }
-> #endif
-131a148,149
-> 
-> #if !defined(__MINGW32__) || HAVE_SECURE_STRING_EXTENSIONS == 0
-135a154,155
-> #endif
-> 
-140a161,173
-> 
-> 
-> // ----------------------------------- THREADS
-> 
-> #if !defined(__MINGW32__) || HAVE_WIN_PTHREAD == 0
-> typedef DWORD pthread_t;
-> typedef DWORD pthread_key_t;
-> typedef LONG pthread_once_t;
-> enum { PTHREAD_ONCE_INIT = 0 };   // important that this be 0! for SpinLock
-> #define pthread_self  GetCurrentThreadId
-> #define pthread_equal(pthread_t_1, pthread_t_2)  ((pthread_t_1)==(pthread_t_2))
-> #endif
-> 
--- a/Resources/Patches/glog-port-h.diff	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
---- /tmp/m/glog-0.3.2/src/windows/port.h	2012-10-03 12:54:10.958149861 +0200
-+++ port.h	2012-10-03 16:19:56.721837994 +0200
-@@ -129,6 +129,27 @@
- #define pthread_self  GetCurrentThreadId
- #define pthread_equal(pthread_t_1, pthread_t_2)  ((pthread_t_1)==(pthread_t_2))
- 
-+#if defined(__MINGW32__)
-+inline int localtime_s(tm * _tm, const time_t * time)
-+{
-+  tm * posix_local_time_struct = localtime(time);
-+  if (posix_local_time_struct == NULL) 
-+  {
-+    return 1;
-+  }
-+
-+  *_tm = *posix_local_time_struct;
-+
-+  return 0;
-+}
-+
-+inline char* strerror_s(char* buf, size_t buflen, int errnum) 
-+{
-+  const char* str = strerror(errnum);
-+  return strncpy(buf, str, buflen - 1);
-+}
-+#endif
-+
- inline struct tm* localtime_r(const time_t* timep, struct tm* result) {
-   localtime_s(result, timep);
-   return result;
--- a/Resources/Patches/glog-utilities-lsb.diff	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
---- utilities.cc.orig	2012-01-12 09:40:21.000000000 +0100
-+++ utilities.cc	2013-09-23 17:37:35.033275313 +0200
-@@ -233,40 +233,7 @@
- }
- 
- pid_t GetTID() {
--  // On Linux and FreeBSD, we try to use gettid().
--#if defined OS_LINUX || defined OS_FREEBSD || defined OS_MACOSX
--#ifndef __NR_gettid
--#ifdef OS_MACOSX
--#define __NR_gettid SYS_gettid
--#elif ! defined __i386__
--#error "Must define __NR_gettid for non-x86 platforms"
--#else
--#define __NR_gettid 224
--#endif
--#endif
--  static bool lacks_gettid = false;
--  if (!lacks_gettid) {
--    pid_t tid = syscall(__NR_gettid);
--    if (tid != -1) {
--      return tid;
--    }
--    // Technically, this variable has to be volatile, but there is a small
--    // performance penalty in accessing volatile variables and there should
--    // not be any serious adverse effect if a thread does not immediately see
--    // the value change to "true".
--    lacks_gettid = true;
--  }
--#endif  // OS_LINUX || OS_FREEBSD
--
--  // If gettid() could not be used, we use one of the following.
--#if defined OS_LINUX
--  return getpid();  // Linux:  getpid returns thread ID when gettid is absent
--#elif defined OS_WINDOWS || defined OS_CYGWIN
--  return GetCurrentThreadId();
--#else
--  // If none of the techniques above worked, we use pthread_self().
-   return (pid_t)(uintptr_t)pthread_self();
--#endif
- }
- 
- const char* const_basename(const char* filepath) {
-@@ -295,7 +262,7 @@
-     g_my_user_name = "invalid-user";
-   }
- }
--REGISTER_MODULE_INITIALIZER(utilities, MyUserNameInitializer());
-+REGISTER_MODULE_INITIALIZER(utilities, MyUserNameInitializer())
- 
- #ifdef HAVE_STACKTRACE
- void DumpStackTraceToString(string* stacktrace) {
--- a/Resources/Patches/glog-utilities.diff	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
---- /tmp/m/glog-0.3.2/src/utilities.cc	2012-01-12 09:40:21.000000000 +0100
-+++ utilities.cc	2012-10-03 16:04:19.745861665 +0200
-@@ -295,7 +295,7 @@
-     g_my_user_name = "invalid-user";
-   }
- }
-+REGISTER_MODULE_INITIALIZER(utilities, MyUserNameInitializer())
--REGISTER_MODULE_INITIALIZER(utilities, MyUserNameInitializer());
- 
- #ifdef HAVE_STACKTRACE
- void DumpStackTraceToString(string* stacktrace) {
--- a/Resources/Patches/glog-visual-studio-port.h	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,178 +0,0 @@
-/* Copyright (c) 2008, Google Inc.
- * All rights reserved.
- * 
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- * 
- *     * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *     * Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following disclaimer
- * in the documentation and/or other materials provided with the
- * distribution.
- *     * Neither the name of Google Inc. nor the names of its
- * contributors may be used to endorse or promote products derived from
- * this software without specific prior written permission.
- * 
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * ---
- * Author: Craig Silverstein
- * Copied from google-perftools and modified by Shinichiro Hamaji
- *
- * These are some portability typedefs and defines to make it a bit
- * easier to compile this code under VC++.
- *
- * Several of these are taken from glib:
- *    http://developer.gnome.org/doc/API/glib/glib-windows-compatability-functions.html
- */
-
-#ifndef CTEMPLATE_WINDOWS_PORT_H_
-#define CTEMPLATE_WINDOWS_PORT_H_
-
-#include "config.h"
-
-#ifdef _WIN32
-
-#define WIN32_LEAN_AND_MEAN  /* We always want minimal includes */
-#include <windows.h>
-#include <winsock.h>         /* for gethostname */
-#include <io.h>              /* because we so often use open/close/etc */
-#include <direct.h>          /* for _getcwd() */
-#include <process.h>         /* for _getpid() */
-#include <stdio.h>           /* read in vsnprintf decl. before redifining it */
-#include <stdarg.h>          /* template_dictionary.cc uses va_copy */
-#include <string.h>          /* for _strnicmp(), strerror_s() */
-#include <time.h>            /* for localtime_s() */
-/* Note: the C++ #includes are all together at the bottom.  This file is
- * used by both C and C++ code, so we put all the C++ together.
- */
-
-// Fix by Sebastien Jodogne for Visual Studio 2013
-// https://code.google.com/p/google-glog/issues/detail?id=212
-#if defined(_MSC_VER) && (_MSC_VER >= 1800)
-#include <algorithm>
-#endif
-
-/* 4244: otherwise we get problems when substracting two size_t's to an int
- * 4251: it's complaining about a private struct I've chosen not to dllexport
- * 4355: we use this in a constructor, but we do it safely
- * 4715: for some reason VC++ stopped realizing you can't return after abort()
- * 4800: we know we're casting ints/char*'s to bools, and we're ok with that
- * 4996: Yes, we're ok using "unsafe" functions like fopen() and strerror()
- */
-#pragma warning(disable:4244 4251 4355 4715 4800 4996)
-
-/* file I/O */
-#define PATH_MAX 1024
-#define access  _access
-#define getcwd  _getcwd
-#define open    _open
-#define read    _read
-#define write   _write
-#define lseek   _lseek
-#define close   _close
-#define popen   _popen
-#define pclose  _pclose
-#define R_OK    04           /* read-only (for access()) */
-#define S_ISDIR(m)  (((m) & _S_IFMT) == _S_IFDIR)
-#ifndef __MINGW32__
-enum { STDIN_FILENO = 0, STDOUT_FILENO = 1, STDERR_FILENO = 2 };
-#endif
-#define S_IRUSR S_IREAD
-#define S_IWUSR S_IWRITE
-
-/* Not quite as lightweight as a hard-link, but more than good enough for us. */
-#define link(oldpath, newpath)  CopyFileA(oldpath, newpath, false)
-
-#define strcasecmp   _stricmp
-#define strncasecmp  _strnicmp
-
-/* In windows-land, hash<> is called hash_compare<> (from xhash.h) */
-#if _MSC_VER < 1700
-#define hash  hash_compare
-#endif
-
-/* Sleep is in ms, on windows */
-#define sleep(secs)  Sleep((secs) * 1000)
-
-/* We can't just use _vsnprintf and _snprintf as drop-in-replacements,
- * because they don't always NUL-terminate. :-(  We also can't use the
- * name vsnprintf, since windows defines that (but not snprintf (!)).
- */
-extern int snprintf(char *str, size_t size,
-                                       const char *format, ...);
-extern int safe_vsnprintf(char *str, size_t size,
-                          const char *format, va_list ap);
-#define vsnprintf(str, size, format, ap)  safe_vsnprintf(str, size, format, ap)
-#define va_copy(dst, src)  (dst) = (src)
-
-/* Windows doesn't support specifying the number of buckets as a
- * hash_map constructor arg, so we leave this blank.
- */
-#define CTEMPLATE_SMALL_HASHTABLE
-
-#define DEFAULT_TEMPLATE_ROOTDIR  ".."
-
-// ----------------------------------- SYSTEM/PROCESS
-typedef int pid_t;
-#define getpid  _getpid
-
-// ----------------------------------- THREADS
-typedef DWORD pthread_t;
-typedef DWORD pthread_key_t;
-typedef LONG pthread_once_t;
-enum { PTHREAD_ONCE_INIT = 0 };   // important that this be 0! for SpinLock
-#define pthread_self  GetCurrentThreadId
-#define pthread_equal(pthread_t_1, pthread_t_2)  ((pthread_t_1)==(pthread_t_2))
-
-#if defined(__MINGW32__)
-inline int localtime_s(tm * _tm, const time_t * time)
-{
-  tm * posix_local_time_struct = localtime(time);
-  if (posix_local_time_struct == NULL) 
-  {
-    return 1;
-  }
-
-  *_tm = *posix_local_time_struct;
-
-  return 0;
-}
-
-inline char* strerror_s(char* buf, size_t buflen, int errnum) 
-{
-  const char* str = strerror(errnum);
-  return strncpy(buf, str, buflen - 1);
-}
-#endif
-
-inline struct tm* localtime_r(const time_t* timep, struct tm* result) {
-  localtime_s(result, timep);
-  return result;
-}
-
-inline char* strerror_r(int errnum, char* buf, size_t buflen) {
-  strerror_s(buf, buflen, errnum);
-  return buf;
-}
-
-#ifndef __cplusplus
-/* I don't see how to get inlining for C code in MSVC.  Ah well. */
-#define inline
-#endif
-
-#endif  /* _WIN32 */
-
-#endif  /* CTEMPLATE_WINDOWS_PORT_H_ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/libp11-0.4.0.patch	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,36 @@
+diff -urEb libp11-0.4.0.orig/src/atfork.c libp11-0.4.0/src/atfork.c
+--- libp11-0.4.0.orig/src/atfork.c	2016-06-20 13:38:43.845575107 +0200
++++ libp11-0.4.0/src/atfork.c	2016-06-20 13:46:52.969575591 +0200
+@@ -25,7 +25,7 @@
+ #include <sys/stat.h>
+ #include <sys/types.h>
+ #include <unistd.h>
+-#include <atfork.h>
++#include "atfork.h"
+ 
+ #ifdef __sun
+ # pragma fini(lib_deinit)
+diff -urEb libp11-0.4.0.orig/src/engine.h libp11-0.4.0/src/engine.h
+--- libp11-0.4.0.orig/src/engine.h	2016-06-20 13:38:43.845575107 +0200
++++ libp11-0.4.0/src/engine.h	2016-06-20 13:46:27.421575566 +0200
+@@ -29,7 +29,7 @@
+ #define _ENGINE_PKCS11_H
+ 
+ #ifndef _WIN32
+-#include "config.h"
++//#include "config.h"
+ #endif
+ 
+ #include "libp11.h"
+diff -urEb libp11-0.4.0.orig/src/libp11-int.h libp11-0.4.0/src/libp11-int.h
+--- libp11-0.4.0.orig/src/libp11-int.h	2016-06-20 13:38:43.845575107 +0200
++++ libp11-0.4.0/src/libp11-int.h	2016-06-20 13:46:27.421575566 +0200
+@@ -20,7 +20,7 @@
+ #define _LIBP11_INT_H
+ 
+ #ifndef _WIN32
+-#include "config.h"
++//#include "config.h"
+ #endif
+ 
+ #include "libp11.h"
--- a/Resources/Patches/log4cplus-patch.diff	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
---- log4cplus-1.0.4.1/src/factory.cxx   2010-05-28 11:06:51.000000000 +0200
-+++ factory.cxx    2012-10-02 11:43:31.808439489 +0200
-@@ -35,7 +35,7 @@
- #  if defined (LOG4CPLUS_HAVE_WIN32_CONSOLE)
- #    include <log4cplus/win32consoleappender.h>
- #  endif
--#  include <log4cplus/Win32DebugAppender.h>
-+#  include <log4cplus/win32debugappender.h>
- #endif
- 
--- a/Resources/Patches/mongoose-3.1-patch.diff	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Patches/mongoose-3.1-patch.diff	Fri Oct 12 15:18:10 2018 +0200
@@ -42,7 +42,7 @@
  
    // Wait until mg_fini() stops
    while (ctx->stop_flag != 2) {
-+#if defined(__linux)
++#if defined(__linux__)
 +    usleep(100000);
 +#elif defined(_WIN32)
 +    Sleep(100);
--- a/Resources/Patches/openssl-lsb.diff	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +0,0 @@
---- ui_openssl.c.orig	2013-09-24 15:06:54.264420779 +0200
-+++ ui_openssl.c	2013-09-24 14:22:43.512312998 +0200
-@@ -291,7 +291,7 @@
- static unsigned short channel = 0;
- #else
- #if !defined(OPENSSL_SYS_MSDOS) || defined(__DJGPP__)
--static TTY_STRUCT tty_orig,tty_new;
-+//static TTY_STRUCT tty_orig,tty_new;
- #endif
- #endif
- static FILE *tty_in, *tty_out;
-@@ -475,106 +475,21 @@
- /* Internal functions to open, handle and close a channel to the console.  */
- static int open_console(UI *ui)
- 	{
--	CRYPTO_w_lock(CRYPTO_LOCK_UI);
--	is_a_tty = 1;
--
--#if defined(OPENSSL_SYS_MACINTOSH_CLASSIC) || defined(OPENSSL_SYS_VXWORKS) || defined(OPENSSL_SYS_NETWARE) || defined(OPENSSL_SYS_BEOS)
--	tty_in=stdin;
--	tty_out=stderr;
--#else
--#  ifdef OPENSSL_SYS_MSDOS
--#    define DEV_TTY "con"
--#  else
--#    define DEV_TTY "/dev/tty"
--#  endif
--	if ((tty_in=fopen(DEV_TTY,"r")) == NULL)
--		tty_in=stdin;
--	if ((tty_out=fopen(DEV_TTY,"w")) == NULL)
--		tty_out=stderr;
--#endif
--
--#if defined(TTY_get) && !defined(OPENSSL_SYS_VMS)
-- 	if (TTY_get(fileno(tty_in),&tty_orig) == -1)
--		{
--#ifdef ENOTTY
--		if (errno == ENOTTY)
--			is_a_tty=0;
--		else
--#endif
--#ifdef EINVAL
--		/* Ariel Glenn ariel@columbia.edu reports that solaris
--		 * can return EINVAL instead.  This should be ok */
--		if (errno == EINVAL)
--			is_a_tty=0;
--		else
--#endif
--			return 0;
--		}
--#endif
--#ifdef OPENSSL_SYS_VMS
--	status = sys$assign(&terminal,&channel,0,0);
--	if (status != SS$_NORMAL)
--		return 0;
--	status=sys$qiow(0,channel,IO$_SENSEMODE,&iosb,0,0,tty_orig,12,0,0,0,0);
--	if ((status != SS$_NORMAL) || (iosb.iosb$w_value != SS$_NORMAL))
--		return 0;
--#endif
- 	return 1;
- 	}
- 
- static int noecho_console(UI *ui)
- 	{
--#ifdef TTY_FLAGS
--	memcpy(&(tty_new),&(tty_orig),sizeof(tty_orig));
--	tty_new.TTY_FLAGS &= ~ECHO;
--#endif
--
--#if defined(TTY_set) && !defined(OPENSSL_SYS_VMS)
--	if (is_a_tty && (TTY_set(fileno(tty_in),&tty_new) == -1))
--		return 0;
--#endif
--#ifdef OPENSSL_SYS_VMS
--	tty_new[0] = tty_orig[0];
--	tty_new[1] = tty_orig[1] | TT$M_NOECHO;
--	tty_new[2] = tty_orig[2];
--	status = sys$qiow(0,channel,IO$_SETMODE,&iosb,0,0,tty_new,12,0,0,0,0);
--	if ((status != SS$_NORMAL) || (iosb.iosb$w_value != SS$_NORMAL))
--		return 0;
--#endif
- 	return 1;
- 	}
- 
- static int echo_console(UI *ui)
- 	{
--#if defined(TTY_set) && !defined(OPENSSL_SYS_VMS)
--	memcpy(&(tty_new),&(tty_orig),sizeof(tty_orig));
--	tty_new.TTY_FLAGS |= ECHO;
--#endif
--
--#if defined(TTY_set) && !defined(OPENSSL_SYS_VMS)
--	if (is_a_tty && (TTY_set(fileno(tty_in),&tty_new) == -1))
--		return 0;
--#endif
--#ifdef OPENSSL_SYS_VMS
--	tty_new[0] = tty_orig[0];
--	tty_new[1] = tty_orig[1] & ~TT$M_NOECHO;
--	tty_new[2] = tty_orig[2];
--	status = sys$qiow(0,channel,IO$_SETMODE,&iosb,0,0,tty_new,12,0,0,0,0);
--	if ((status != SS$_NORMAL) || (iosb.iosb$w_value != SS$_NORMAL))
--		return 0;
--#endif
- 	return 1;
- 	}
- 
- static int close_console(UI *ui)
- 	{
--	if (tty_in != stdin) fclose(tty_in);
--	if (tty_out != stderr) fclose(tty_out);
--#ifdef OPENSSL_SYS_VMS
--	status = sys$dassgn(channel);
--#endif
--	CRYPTO_w_unlock(CRYPTO_LOCK_UI);
--
- 	return 1;
- 	}
- 
--- a/Resources/RetrieveCACertificates.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/RetrieveCACertificates.py	Fri Oct 12 15:18:10 2018 +0200
@@ -1,8 +1,9 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Fri Oct 12 15:18:10 2018 +0200
@@ -1,8 +1,9 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -33,7 +34,7 @@
 
 Usage: %s [hostname] [HTTP port] [path]
 Usage: %s [hostname] [HTTP port] [path] [username] [password]
-For instance: %s localhost 8042 .
+For instance: %s 127.0.0.1 8042 .
 """ % (sys.argv[0], sys.argv[0], sys.argv[0]))
     exit(-1)
 
@@ -81,7 +82,7 @@
             sys.stdout.write(" => success\n")
             success_count += 1
         else:
-            sys.stdout.write(" => failure (Is it a DICOM file?)\n")
+            sys.stdout.write(" => failure (Is it a DICOM file? Is there a password?)\n")
 
     except:
         sys.stdout.write(" => unable to connect (Is Orthanc running? Is there a password?)\n")
--- a/Resources/Samples/Lua/AutomatedJpeg2kCompression.lua	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Lua/AutomatedJpeg2kCompression.lua	Fri Oct 12 15:18:10 2018 +0200
@@ -16,7 +16,7 @@
 
       -- Compress to JPEG2000 using gdcm
       local compressed = instanceId .. '-compressed.dcm'
-      os.execute('gdcmconv --j2k ' .. uncompressed .. ' ' .. compressed)
+      os.execute('gdcmconv -U --j2k ' .. uncompressed .. ' ' .. compressed)
 
       -- Generate a new SOPInstanceUID for the JPEG2000 file, as
       -- gdcmconv does not do this by itself
--- a/Resources/Samples/Lua/AutoroutingModification.lua	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Lua/AutoroutingModification.lua	Fri Oct 12 15:18:10 2018 +0200
@@ -1,8 +1,7 @@
-function OnStoredInstance(instanceId, tags, metadata)
-   -- Ignore the instances that result from a modification to avoid
-   -- infinite loops
-   if (metadata['ModifiedFrom'] == nil and
-       metadata['AnonymizedFrom'] == nil) then
+function OnStoredInstance(instanceId, tags, metadata, origin)
+   -- Ignore the instances that result from the present Lua script to
+   -- avoid infinite loops
+   if origin['RequestOrigin'] ~= 'Lua' then
 
       -- The tags to be replaced
       local replace = {}
@@ -12,10 +11,21 @@
       -- The tags to be removed
       local remove = { 'MilitaryRank' }
 
-      -- Modify the instance, send it, then delete the modified instance
-      Delete(SendToModality(ModifyInstance(instanceId, replace, remove, true), 'sample'))
+      -- Modify the instance
+      local command = {}
+      command['Replace'] = replace
+      command['Remove'] = remove
+      local modifiedFile = RestApiPost('/instances/' .. instanceId .. '/modify', DumpJson(command, true))
 
-      -- Delete the original instance
-      Delete(instanceId)
+      -- Upload the modified instance to the Orthanc database so that
+      -- it can be sent by Orthanc to other modalities
+      local modifiedId = ParseJson(RestApiPost('/instances/', modifiedFile)) ['ID']
+
+      -- Send the modified instance to another modality
+      RestApiPost('/modalities/sample/store', modifiedId)
+
+      -- Delete the original and the modified instances
+      RestApiDelete('/instances/' .. instanceId)
+      RestApiDelete('/instances/' .. modifiedId)
    end
 end
--- a/Resources/Samples/Lua/CallWebService.js	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Lua/CallWebService.js	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Resources/Samples/Lua/CallWebService.lua	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Lua/CallWebService.lua	Fri Oct 12 15:18:10 2018 +0200
@@ -17,7 +17,7 @@
    info['PatientID'] = tags['PatientID']
 
    -- Send the POST request
-   local answer = HttpPost('http://localhost:8000/', JSON:encode(info))
+   local answer = HttpPost('http://127.0.0.1:8000/', JSON:encode(info))
 
    -- The answer equals "ERROR" in case of an error
    print('Web service called, answer received: ' .. answer)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Lua/IncomingFindRequestFilter.lua	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,14 @@
+-- This example solves the following problem:
+-- https://groups.google.com/d/msg/orthanc-users/PLWKqVVaXLs/n_0x4vKhAgAJ
+
+function IncomingFindRequestFilter(source, origin)
+   -- First display the content of the C-Find query
+   PrintRecursive(source)
+   PrintRecursive(origin)
+
+   -- Remove the "PrivateCreator" tag from the query
+   local v = source
+   v['5555,0010'] = nil
+
+   return v
+end
--- a/Resources/Samples/Lua/ModifyInstanceWithSequence.lua	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Lua/ModifyInstanceWithSequence.lua	Fri Oct 12 15:18:10 2018 +0200
@@ -17,7 +17,7 @@
 
       -- Create the modified instance
       local modified = RestApiPost('/instances/' .. instanceId .. '/modify',
-                                   DumpJson(request))
+                                   DumpJson(request, true))
 
       -- Upload the modified instance to the Orthanc store
       RestApiPost('/instances/', modified)
--- a/Resources/Samples/Lua/OnStableStudy.lua	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Lua/OnStableStudy.lua	Fri Oct 12 15:18:10 2018 +0200
@@ -40,7 +40,7 @@
 
       -- Modify the entire study in one single call
       local m = RestApiPost('/studies/' .. studyId .. '/modify',
-                            DumpJson(command))
+                            DumpJson(command, true))
       print('Modified study: ' .. m)
    end
 end
--- a/Resources/Samples/Lua/TransferSyntaxDisable.lua	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Lua/TransferSyntaxDisable.lua	Fri Oct 12 15:18:10 2018 +0200
@@ -26,4 +26,8 @@
    return false
 end
 
+function IsUnknownSopClassAccepted(aet, ip)
+   return false
+end
+
 print('All special transfer syntaxes are now disallowed')
--- a/Resources/Samples/Lua/TransferSyntaxEnable.lua	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Lua/TransferSyntaxEnable.lua	Fri Oct 12 15:18:10 2018 +0200
@@ -26,4 +26,8 @@
    return true
 end
 
+function IsUnknownSopClassAccepted(aet, ip)
+   return true
+end
+
 print('All special transfer syntaxes are now accepted')
--- a/Resources/Samples/Lua/WriteToDisk.lua	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Lua/WriteToDisk.lua	Fri Oct 12 15:18:10 2018 +0200
@@ -2,7 +2,8 @@
 
 function ToAscii(s)
    -- http://www.lua.org/manual/5.1/manual.html#pdf-string.gsub
-   return s:gsub('[^a-zA-Z0-9-/ ]', '_')
+   -- https://groups.google.com/d/msg/orthanc-users/qMLgkEmwwPI/6jRpCrlgBwAJ
+   return s:gsub('[^a-zA-Z0-9-/-: ]', '_')
 end
 
 function OnStableSeries(seriesId, tags, metadata)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/OrthancFramework/MicroService/CMakeLists.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,16 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(Sample)
+
+include(${CMAKE_SOURCE_DIR}/../../../CMake/OrthancFrameworkParameters.cmake)
+
+set(ENABLE_WEB_SERVER ON)
+
+include(${CMAKE_SOURCE_DIR}/../../../CMake/OrthancFrameworkConfiguration.cmake)
+
+add_executable(Sample
+  ${ORTHANC_CORE_SOURCES}
+  Sample.cpp
+  )
+
+include_directories(${ORTHANC_ROOT}/Core)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/OrthancFramework/MicroService/README.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,2 @@
+This file shows how to create a simple Web service in C++ (similar to
+Python's Flask) using the Orthanc standalone framework.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/OrthancFramework/MicroService/Sample.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,61 @@
+#include <stdio.h>
+
+#include <HttpServer/MongooseServer.h>
+#include <Logging.h>
+#include <RestApi/RestApi.h>
+#include <SystemToolbox.h>
+
+class MicroService : public Orthanc::RestApi
+{
+private:
+  static MicroService& GetSelf(Orthanc::RestApiCall& call)
+  {
+    return dynamic_cast<MicroService&>(call.GetContext());
+  }
+
+  void SayHello()
+  {
+    printf("Hello\n");
+  }
+
+  static void Hello(Orthanc::RestApiGetCall& call)
+  {
+    GetSelf(call).SayHello();
+    
+    Json::Value value = Json::arrayValue;
+    value.append("World");
+    
+    call.GetOutput().AnswerJson(value);
+  }
+
+public:
+  MicroService()
+  {
+    Register("/hello", Hello);
+  }  
+};
+
+int main()
+{
+  Orthanc::Logging::Initialize();
+  Orthanc::Logging::EnableTraceLevel(true);
+
+  MicroService rest;
+  
+  {
+    Orthanc::MongooseServer httpServer;
+    httpServer.SetPortNumber(8000);
+    httpServer.Register(rest);
+    httpServer.SetRemoteAccessAllowed(true);
+    httpServer.Start();
+    
+    LOG(WARNING) << "Micro-service started on port " << httpServer.GetPortNumber();
+    Orthanc::SystemToolbox::ServerBarrier();
+  }
+
+  LOG(WARNING) << "Micro-service stopped";
+
+  Orthanc::Logging::Finalize();
+  
+  return 0;
+}
--- a/Resources/Samples/Python/AnonymizeAllPatients.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Python/AnonymizeAllPatients.py	Fri Oct 12 15:18:10 2018 +0200
@@ -2,8 +2,9 @@
 # -*- coding: utf-8 -*-
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -20,7 +21,7 @@
 
 
 
-URL = 'http://localhost:8042'
+URL = 'http://127.0.0.1:8042'
 
 #
 # This sample code will anonymize all the patients that are stored in
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Python/ArchiveAllPatients.py	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,48 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+
+import os
+import os.path
+import sys
+import RestToolbox
+
+def PrintHelp():
+    print('Download one ZIP archive for all the patients stored in Orthanc\n')
+    print('Usage: %s <URL> <Target>\n' % sys.argv[0])
+    print('Example: %s http://127.0.0.1:8042/ /tmp/Archive.zip\n' % sys.argv[0])
+    exit(-1)
+
+if len(sys.argv) != 3:
+    PrintHelp()
+
+URL = sys.argv[1]
+TARGET = sys.argv[2]
+
+patients = RestToolbox.DoGet('%s/patients' % URL)
+
+print('Downloading ZIP...')
+zipContent = RestToolbox.DoPost('%s/tools/create-archive' % URL, patients)
+
+# Write the ZIP archive at the proper location
+with open(TARGET, 'wb') as f:
+    f.write(zipContent)
--- a/Resources/Samples/Python/ArchiveStudiesInTimeRange.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Python/ArchiveStudiesInTimeRange.py	Fri Oct 12 15:18:10 2018 +0200
@@ -2,8 +2,9 @@
 # -*- coding: utf-8 -*-
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -29,7 +30,7 @@
     print('Download ZIP archives for all the studies generated '
           'during a given time range (according to the StudyDate tag)\n')
     print('Usage: %s <URL> <StartDate> <EndDate> <TargetFolder>\n' % sys.argv[0])
-    print('Example: %s http://localhost:8042/ 20150101 20151231 /tmp/\n' % sys.argv[0])
+    print('Example: %s http://127.0.0.1:8042/ 20150101 20151231 /tmp/\n' % sys.argv[0])
     exit(-1)
 
 def CheckIsDate(date):
@@ -49,6 +50,12 @@
 CheckIsDate(START)
 CheckIsDate(END)
 
+def GetTag(tags, key):
+    if key in tags:
+        return tags[key]
+    else:
+        return 'No%s' % key
+
 # Loop over the studies
 for studyId in RestToolbox.DoGet('%s/studies' % URL):
     # Retrieve the DICOM tags of the current study
@@ -61,13 +68,13 @@
     studyDate = study['StudyDate'][:8]
     if studyDate >= START and studyDate <= END:
         # Create a filename
-        filename = '%s - %s %s - %s.zip' % (study['StudyDate'],
-                                            patient['PatientID'],
-                                            patient['PatientName'],
-                                            study['StudyDescription'])
+        filename = '%s - %s %s - %s.zip' % (GetTag(study, 'StudyDate'),
+                                            GetTag(patient, 'PatientID'),
+                                            GetTag(patient, 'PatientName'),
+                                            GetTag(study, 'StudyDescription'))
 
         # Remove any non-ASCII character in the filename
-        filename = filename.encode('ascii', errors = 'replace')
+        filename = filename.encode('ascii', errors = 'replace').translate(None, r"'\/:*?\"<>|!=").strip()
 
         # Download the ZIP archive of the study
         print('Downloading %s' % filename)
--- a/Resources/Samples/Python/AutoClassify.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Python/AutoClassify.py	Fri Oct 12 15:18:10 2018 +0200
@@ -2,8 +2,9 @@
 # -*- coding: utf-8 -*-
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -30,7 +31,7 @@
     description = 'Automated classification of DICOM files from Orthanc.',
     formatter_class = argparse.ArgumentDefaultsHelpFormatter)
 
-parser.add_argument('--host', default = 'localhost',
+parser.add_argument('--host', default = '127.0.0.1',
                     help = 'The host address that runs Orthanc')
 parser.add_argument('--port', type = int, default = '8042',
                     help = 'The port number to which Orthanc is listening for the REST API')
@@ -45,7 +46,7 @@
 
 
 def FixPath(p):
-    return p.encode('ascii', 'ignore').strip().decode()
+    return p.encode('ascii', errors = 'replace').translate(None, r"'\/:*?\"<>|!=").strip()
 
 def GetTag(resource, tag):
     if ('MainDicomTags' in resource and
@@ -110,13 +111,17 @@
         if change['ChangeType'] == 'NewInstance':
             try:
                 ClassifyInstance(change['ID'])
+
+                # If requested, remove the instance once it has been
+                # properly handled by "ClassifyInstance()". Thanks to
+                # the "try/except" block, the instance is not removed
+                # if the "ClassifyInstance()" function fails.
+                if args.remove:
+                    RestToolbox.DoDelete('%s/instances/%s' % (URL, change['ID']))
+
             except:
                 print('Unable to write instance %s to the disk' % change['ID'])
 
-            # If requested, remove the instance once it has been copied
-            if args.remove:
-                RestToolbox.DoDelete('%s/instances/%s' % (URL, change['ID']))
-
     current = r['Last']
 
     if r['Done']:
--- a/Resources/Samples/Python/ChangesLoop.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Python/ChangesLoop.py	Fri Oct 12 15:18:10 2018 +0200
@@ -1,8 +1,9 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -34,7 +35,7 @@
 images into Orthanc (through the Changes API).
 
 Usage: %s [hostname] [HTTP port]
-For instance: %s localhost 8042
+For instance: %s 127.0.0.1 8042
 """ % (sys.argv[0], sys.argv[0]))
     exit(-1)
 
--- a/Resources/Samples/Python/ContinuousPatientAnonymization.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Python/ContinuousPatientAnonymization.py	Fri Oct 12 15:18:10 2018 +0200
@@ -1,8 +1,9 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -37,7 +38,7 @@
 the configuration option "StableAge").
 
 Usage: %s [hostname] [HTTP port]
-For instance: %s localhost 8042
+For instance: %s 127.0.0.1 8042
 """ % (sys.argv[0], sys.argv[0]))
     exit(-1)
 
--- a/Resources/Samples/Python/DownloadAnonymized.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Python/DownloadAnonymized.py	Fri Oct 12 15:18:10 2018 +0200
@@ -2,8 +2,9 @@
 # -*- coding: utf-8 -*-
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -20,7 +21,7 @@
 
 
 
-URL = 'http://localhost:8042'
+URL = 'http://127.0.0.1:8042'
 
 #
 # This sample code will download a ZIP file for each patient that has
@@ -44,6 +45,5 @@
         # Trigger the download
         print('Downloading %s' % name)
         zipContent = RestToolbox.DoGet('%s/patients/%s/archive' % (URL, patient))
-        f = open(os.path.join('/tmp', name + '.zip'), 'wb')
-        f.write(zipContent)
-        f.close()
+        with open(os.path.join('/tmp', name + '.zip'), 'wb') as f:
+            f.write(zipContent)
--- a/Resources/Samples/Python/HighPerformanceAutoRouting.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Python/HighPerformanceAutoRouting.py	Fri Oct 12 15:18:10 2018 +0200
@@ -2,8 +2,9 @@
 # -*- coding: utf-8 -*-
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -20,7 +21,7 @@
 
 
 
-URL = 'http://localhost:8042'
+URL = 'http://127.0.0.1:8042'
 TARGET = 'sample'
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Python/ManualModification.py	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,68 @@
+#!/usr/bin/python
+
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+
+# This sample shows how to carry on a manual modification of DICOM
+# tags spread accross various levels (Patient/Study/Series/Instance)
+# that would normally forbidden as such by the REST API of Orthanc to
+# avoid breaking the DICOM hierarchy. This sample can be useful for
+# more complex anonymization/modification scenarios, or for optimizing
+# the disk usage (the original and the modified instances never
+# coexist).
+
+from RestToolbox import *
+
+URL = 'http://127.0.0.1:8042'
+STUDY = '27f7126f-4f66fb14-03f4081b-f9341db2-53925988'
+
+identifiers = {}
+
+for instance in DoGet('%s/studies/%s/instances' % (URL, STUDY)):
+    # Setup the parameters of the modification
+    replace = { 
+        "PatientID" : "Hello",
+        "PatientName" : "Modified",
+        "StationName" : "TEST",
+    }
+
+    # Get the original UIDs of the instance
+    seriesUID = DoGet('%s/instances/%s/content/SeriesInstanceUID' % (URL, instance['ID']))
+    if seriesUID in identifiers:
+        replace['SeriesInstanceUID'] = identifiers[seriesUID]
+
+    studyUID = DoGet('%s/instances/%s/content/StudyInstanceUID' % (URL, instance['ID']))
+    if studyUID in identifiers:
+        replace['StudyInstanceUID'] = identifiers[studyUID]
+
+    # Manually modify the instance
+    print('Modifying instance %s' % instance['ID'])
+    modified = DoPost('%s/instances/%s/modify' % (URL, instance['ID']),
+                      { "Replace" : replace })
+
+    # Remove the original instance
+    DoDelete('%s/instances/%s' % (URL, instance['ID']))
+
+    # Add the modified instance
+    modifiedId = DoPost('%s/instances' % URL, modified)['ID']
+
+    # Register the modified UIDs
+    identifiers[seriesUID] = DoGet('%s/instances/%s/content/SeriesInstanceUID' % (URL, modifiedId))
+    identifiers[studyUID] = DoGet('%s/instances/%s/content/StudyInstanceUID' % (URL, modifiedId))
--- a/Resources/Samples/Python/Replicate.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Python/Replicate.py	Fri Oct 12 15:18:10 2018 +0200
@@ -1,8 +1,9 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -33,7 +34,7 @@
 server through their REST API.
 
 Usage: %s [SourceURI] [TargetURI]
-For instance: %s http://orthanc:password@localhost:8042/ http://localhost:8043/
+For instance: %s http://orthanc:password@127.0.0.1:8042/ http://127.0.0.1:8043/
 """ % (sys.argv[0], sys.argv[0]))
     exit(-1)
 
--- a/Resources/Samples/Python/RestToolbox.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Python/RestToolbox.py	Fri Oct 12 15:18:10 2018 +0200
@@ -1,6 +1,7 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -48,14 +49,17 @@
     if _credentials != None:
         h.add_credentials(_credentials[0], _credentials[1])
 
-def DoGet(uri, data = {}, interpretAsJson = True):
+def _ComputeGetUri(uri, data):
     d = ''
     if len(data.keys()) > 0:
         d = '?' + urlencode(data)
 
+    return uri + d
+        
+def DoGet(uri, data = {}, interpretAsJson = True):
     h = httplib2.Http()
     _SetupCredentials(h)
-    resp, content = h.request(uri + d, 'GET')
+    resp, content = h.request(_ComputeGetUri(uri, data), 'GET')
     if not (resp.status in [ 200 ]):
         raise Exception(resp.status)
     elif not interpretAsJson:
@@ -64,6 +68,16 @@
         return _DecodeJson(content)
 
 
+def DoRawGet(uri, data = {}):
+    h = httplib2.Http()
+    _SetupCredentials(h)
+    resp, content = h.request(_ComputeGetUri(uri, data), 'GET')
+    if not (resp.status in [ 200 ]):
+        raise Exception(resp.status)
+    else:
+        return content
+
+
 def _DoPutOrPost(uri, method, data, contentType):
     h = httplib2.Http()
     _SetupCredentials(h)
--- a/Resources/Samples/Tools/CMakeLists.txt	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Tools/CMakeLists.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -8,34 +8,29 @@
   link_libraries(pthread dl)
 endif()
 
+include(${CMAKE_SOURCE_DIR}/../../CMake/OrthancFrameworkParameters.cmake)
+
 set(STATIC_BUILD ON)
 set(ALLOW_DOWNLOADS ON)
 
-set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../..)
-
-include(CheckIncludeFiles)
-include(CheckIncludeFileCXX)
-include(CheckLibraryExists)
-include(${ORTHANC_ROOT}/Resources/CMake/Compiler.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/ZlibConfiguration.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfiguration.cmake)
+include(${CMAKE_SOURCE_DIR}/../../CMake/OrthancFrameworkConfiguration.cmake)
 
 add_library(CommonLibraries
   ${BOOST_SOURCES}
-  ${THIRD_PARTY_SOURCES}
-  ${ORTHANC_ROOT}/Core/OrthancException.cpp
+  ${JSONCPP_SOURCES}
+  ${ORTHANC_ROOT}/Core/Enumerations.cpp
+  ${ORTHANC_ROOT}/Core/Logging.cpp
+  ${ORTHANC_ROOT}/Core/SystemToolbox.cpp
   ${ORTHANC_ROOT}/Core/Toolbox.cpp
-  ${ORTHANC_ROOT}/Core/Uuid.cpp
   ${ORTHANC_ROOT}/Resources/ThirdParty/md5/md5.c
   ${ORTHANC_ROOT}/Resources/ThirdParty/base64/base64.cpp
   )
 
 add_executable(RecoverCompressedFile
   RecoverCompressedFile.cpp
-  ${ORTHANC_ROOT}/Core/Compression/BufferCompressor.cpp
+  ${ORTHANC_ROOT}/Core/Compression/DeflateBaseCompressor.cpp
   ${ORTHANC_ROOT}/Core/Compression/ZlibCompressor.cpp
+  ${ZLIB_SOURCES}
   )
 
-target_link_libraries(RecoverCompressedFile CommonLibraries GoogleLog)
+target_link_libraries(RecoverCompressedFile CommonLibraries)
--- a/Resources/Samples/Tools/RecoverCompressedFile.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/Tools/RecoverCompressedFile.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -19,7 +20,7 @@
 
 
 #include "../../../Core/Compression/ZlibCompressor.h"
-#include "../../../Core/Toolbox.h"
+#include "../../../Core/SystemToolbox.h"
 #include "../../../Core/OrthancException.h"
 
 #include <stdio.h>
@@ -40,21 +41,23 @@
     fflush(stderr);
 
     std::string content;
-    Orthanc::Toolbox::ReadFile(content, argv[1]);
+    Orthanc::SystemToolbox::ReadFile(content, argv[1]);
 
     fprintf(stderr, "Decompressing the content of the file...\n");
     fflush(stderr);
 
     Orthanc::ZlibCompressor compressor;
     std::string uncompressed;
-    compressor.Uncompress(uncompressed, content);
+    compressor.Uncompress(uncompressed, 
+                          content.empty() ? NULL : content.c_str(), 
+                          content.size());
 
     fprintf(stderr, "Writing the uncompressed data...\n");
     fflush(stderr);
 
     if (argc == 3)
     {
-      Orthanc::Toolbox::WriteFile(uncompressed, argv[2]);
+      Orthanc::SystemToolbox::WriteFile(uncompressed, argv[2]);
     }
     else
     {
--- a/Resources/Samples/WebApplications/DrawingDicomizer.js	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/WebApplications/DrawingDicomizer.js	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -58,7 +59,7 @@
         toolbox.ServeFile('DrawingDicomizer/orthanc.js', response);
       }
       else if (req.url == '/jquery.js') {
-        toolbox.ServeFile('../../../OrthancExplorer/libs/jquery-1.7.2.min.js', response);
+        toolbox.ServeFile('../../../OrthancExplorer/libs/jquery.min.js', response);
       }
       else if (req.url.startsWith('/orthanc')) {
         toolbox.ForwardGetRequest(orthanc, req.url.substr(8), response);
@@ -96,4 +97,6 @@
   }
 });
 
+
+console.log('The demo is running at http://localhost:' + port + '/');
 server.listen(port);
--- a/Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Resources/Samples/WebApplications/NodeToolbox.js	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/Samples/WebApplications/NodeToolbox.js	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Resources/ThirdParty/VisualStudio/stdint.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/ThirdParty/VisualStudio/stdint.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,247 +1,259 @@
-// ISO C9x  compliant stdint.h for Microsoft Visual Studio
-// Based on ISO/IEC 9899:TC2 Committee draft (May 6, 2005) WG14/N1124 
-// 
-//  Copyright (c) 2006-2008 Alexander Chemeris
-// 
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-// 
-//   1. Redistributions of source code must retain the above copyright notice,
-//      this list of conditions and the following disclaimer.
-// 
-//   2. Redistributions in binary form must reproduce the above copyright
-//      notice, this list of conditions and the following disclaimer in the
-//      documentation and/or other materials provided with the distribution.
-// 
-//   3. The name of the author may be used to endorse or promote products
-//      derived from this software without specific prior written permission.
-// 
-// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
-// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
-// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
-// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
-// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
-// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
-// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
-// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-// 
-///////////////////////////////////////////////////////////////////////////////
-
-#ifndef _MSC_VER // [
-#error "Use this header only with Microsoft Visual C++ compilers!"
-#endif // _MSC_VER ]
-
-#ifndef _MSC_STDINT_H_ // [
-#define _MSC_STDINT_H_
-
-#if _MSC_VER > 1000
-#pragma once
-#endif
-
-#include <limits.h>
-
-// For Visual Studio 6 in C++ mode and for many Visual Studio versions when
-// compiling for ARM we should wrap <wchar.h> include with 'extern "C++" {}'
-// or compiler give many errors like this:
-//   error C2733: second C linkage of overloaded function 'wmemchr' not allowed
-#ifdef __cplusplus
-extern "C" {
-#endif
-#  include <wchar.h>
-#ifdef __cplusplus
-}
-#endif
-
-// Define _W64 macros to mark types changing their size, like intptr_t.
-#ifndef _W64
-#  if !defined(__midl) && (defined(_X86_) || defined(_M_IX86)) && _MSC_VER >= 1300
-#     define _W64 __w64
-#  else
-#     define _W64
-#  endif
-#endif
-
-
-// 7.18.1 Integer types
-
-// 7.18.1.1 Exact-width integer types
-
-// Visual Studio 6 and Embedded Visual C++ 4 doesn't
-// realize that, e.g. char has the same size as __int8
-// so we give up on __intX for them.
-#if (_MSC_VER < 1300)
-   typedef signed char       int8_t;
-   typedef signed short      int16_t;
-   typedef signed int        int32_t;
-   typedef unsigned char     uint8_t;
-   typedef unsigned short    uint16_t;
-   typedef unsigned int      uint32_t;
-#else
-   typedef signed __int8     int8_t;
-   typedef signed __int16    int16_t;
-   typedef signed __int32    int32_t;
-   typedef unsigned __int8   uint8_t;
-   typedef unsigned __int16  uint16_t;
-   typedef unsigned __int32  uint32_t;
-#endif
-typedef signed __int64       int64_t;
-typedef unsigned __int64     uint64_t;
-
-
-// 7.18.1.2 Minimum-width integer types
-typedef int8_t    int_least8_t;
-typedef int16_t   int_least16_t;
-typedef int32_t   int_least32_t;
-typedef int64_t   int_least64_t;
-typedef uint8_t   uint_least8_t;
-typedef uint16_t  uint_least16_t;
-typedef uint32_t  uint_least32_t;
-typedef uint64_t  uint_least64_t;
-
-// 7.18.1.3 Fastest minimum-width integer types
-typedef int8_t    int_fast8_t;
-typedef int16_t   int_fast16_t;
-typedef int32_t   int_fast32_t;
-typedef int64_t   int_fast64_t;
-typedef uint8_t   uint_fast8_t;
-typedef uint16_t  uint_fast16_t;
-typedef uint32_t  uint_fast32_t;
-typedef uint64_t  uint_fast64_t;
-
-// 7.18.1.4 Integer types capable of holding object pointers
-#ifdef _WIN64 // [
-   typedef signed __int64    intptr_t;
-   typedef unsigned __int64  uintptr_t;
-#else // _WIN64 ][
-   typedef _W64 signed int   intptr_t;
-   typedef _W64 unsigned int uintptr_t;
-#endif // _WIN64 ]
-
-// 7.18.1.5 Greatest-width integer types
-typedef int64_t   intmax_t;
-typedef uint64_t  uintmax_t;
-
-
-// 7.18.2 Limits of specified-width integer types
-
-#if !defined(__cplusplus) || defined(__STDC_LIMIT_MACROS) // [   See footnote 220 at page 257 and footnote 221 at page 259
-
-// 7.18.2.1 Limits of exact-width integer types
-#define INT8_MIN     ((int8_t)_I8_MIN)
-#define INT8_MAX     _I8_MAX
-#define INT16_MIN    ((int16_t)_I16_MIN)
-#define INT16_MAX    _I16_MAX
-#define INT32_MIN    ((int32_t)_I32_MIN)
-#define INT32_MAX    _I32_MAX
-#define INT64_MIN    ((int64_t)_I64_MIN)
-#define INT64_MAX    _I64_MAX
-#define UINT8_MAX    _UI8_MAX
-#define UINT16_MAX   _UI16_MAX
-#define UINT32_MAX   _UI32_MAX
-#define UINT64_MAX   _UI64_MAX
-
-// 7.18.2.2 Limits of minimum-width integer types
-#define INT_LEAST8_MIN    INT8_MIN
-#define INT_LEAST8_MAX    INT8_MAX
-#define INT_LEAST16_MIN   INT16_MIN
-#define INT_LEAST16_MAX   INT16_MAX
-#define INT_LEAST32_MIN   INT32_MIN
-#define INT_LEAST32_MAX   INT32_MAX
-#define INT_LEAST64_MIN   INT64_MIN
-#define INT_LEAST64_MAX   INT64_MAX
-#define UINT_LEAST8_MAX   UINT8_MAX
-#define UINT_LEAST16_MAX  UINT16_MAX
-#define UINT_LEAST32_MAX  UINT32_MAX
-#define UINT_LEAST64_MAX  UINT64_MAX
-
-// 7.18.2.3 Limits of fastest minimum-width integer types
-#define INT_FAST8_MIN    INT8_MIN
-#define INT_FAST8_MAX    INT8_MAX
-#define INT_FAST16_MIN   INT16_MIN
-#define INT_FAST16_MAX   INT16_MAX
-#define INT_FAST32_MIN   INT32_MIN
-#define INT_FAST32_MAX   INT32_MAX
-#define INT_FAST64_MIN   INT64_MIN
-#define INT_FAST64_MAX   INT64_MAX
-#define UINT_FAST8_MAX   UINT8_MAX
-#define UINT_FAST16_MAX  UINT16_MAX
-#define UINT_FAST32_MAX  UINT32_MAX
-#define UINT_FAST64_MAX  UINT64_MAX
-
-// 7.18.2.4 Limits of integer types capable of holding object pointers
-#ifdef _WIN64 // [
-#  define INTPTR_MIN   INT64_MIN
-#  define INTPTR_MAX   INT64_MAX
-#  define UINTPTR_MAX  UINT64_MAX
-#else // _WIN64 ][
-#  define INTPTR_MIN   INT32_MIN
-#  define INTPTR_MAX   INT32_MAX
-#  define UINTPTR_MAX  UINT32_MAX
-#endif // _WIN64 ]
-
-// 7.18.2.5 Limits of greatest-width integer types
-#define INTMAX_MIN   INT64_MIN
-#define INTMAX_MAX   INT64_MAX
-#define UINTMAX_MAX  UINT64_MAX
-
-// 7.18.3 Limits of other integer types
-
-#ifdef _WIN64 // [
-#  define PTRDIFF_MIN  _I64_MIN
-#  define PTRDIFF_MAX  _I64_MAX
-#else  // _WIN64 ][
-#  define PTRDIFF_MIN  _I32_MIN
-#  define PTRDIFF_MAX  _I32_MAX
-#endif  // _WIN64 ]
-
-#define SIG_ATOMIC_MIN  INT_MIN
-#define SIG_ATOMIC_MAX  INT_MAX
-
-#ifndef SIZE_MAX // [
-#  ifdef _WIN64 // [
-#     define SIZE_MAX  _UI64_MAX
-#  else // _WIN64 ][
-#     define SIZE_MAX  _UI32_MAX
-#  endif // _WIN64 ]
-#endif // SIZE_MAX ]
-
-// WCHAR_MIN and WCHAR_MAX are also defined in <wchar.h>
-#ifndef WCHAR_MIN // [
-#  define WCHAR_MIN  0
-#endif  // WCHAR_MIN ]
-#ifndef WCHAR_MAX // [
-#  define WCHAR_MAX  _UI16_MAX
-#endif  // WCHAR_MAX ]
-
-#define WINT_MIN  0
-#define WINT_MAX  _UI16_MAX
-
-#endif // __STDC_LIMIT_MACROS ]
-
-
-// 7.18.4 Limits of other integer types
-
-#if !defined(__cplusplus) || defined(__STDC_CONSTANT_MACROS) // [   See footnote 224 at page 260
-
-// 7.18.4.1 Macros for minimum-width integer constants
-
-#define INT8_C(val)  val##i8
-#define INT16_C(val) val##i16
-#define INT32_C(val) val##i32
-#define INT64_C(val) val##i64
-
-#define UINT8_C(val)  val##ui8
-#define UINT16_C(val) val##ui16
-#define UINT32_C(val) val##ui32
-#define UINT64_C(val) val##ui64
-
-// 7.18.4.2 Macros for greatest-width integer constants
-#define INTMAX_C   INT64_C
-#define UINTMAX_C  UINT64_C
-
-#endif // __STDC_CONSTANT_MACROS ]
-
-
-#endif // _MSC_STDINT_H_ ]
+// ISO C9x  compliant stdint.h for Microsoft Visual Studio
+// Based on ISO/IEC 9899:TC2 Committee draft (May 6, 2005) WG14/N1124 
+// 
+//  Copyright (c) 2006-2013 Alexander Chemeris
+// 
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+// 
+//   1. Redistributions of source code must retain the above copyright notice,
+//      this list of conditions and the following disclaimer.
+// 
+//   2. Redistributions in binary form must reproduce the above copyright
+//      notice, this list of conditions and the following disclaimer in the
+//      documentation and/or other materials provided with the distribution.
+// 
+//   3. Neither the name of the product nor the names of its contributors may
+//      be used to endorse or promote products derived from this software
+//      without specific prior written permission.
+// 
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+// 
+///////////////////////////////////////////////////////////////////////////////
+
+#ifndef _MSC_VER // [
+#error "Use this header only with Microsoft Visual C++ compilers!"
+#endif // _MSC_VER ]
+
+#ifndef _MSC_STDINT_H_ // [
+#define _MSC_STDINT_H_
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+#if _MSC_VER >= 1600 // [
+#include <stdint.h>
+#else // ] _MSC_VER >= 1600 [
+
+#include <limits.h>
+
+// For Visual Studio 6 in C++ mode and for many Visual Studio versions when
+// compiling for ARM we should wrap <wchar.h> include with 'extern "C++" {}'
+// or compiler give many errors like this:
+//   error C2733: second C linkage of overloaded function 'wmemchr' not allowed
+#ifdef __cplusplus
+extern "C" {
+#endif
+#  include <wchar.h>
+#ifdef __cplusplus
+}
+#endif
+
+// Define _W64 macros to mark types changing their size, like intptr_t.
+#ifndef _W64
+#  if !defined(__midl) && (defined(_X86_) || defined(_M_IX86)) && _MSC_VER >= 1300
+#     define _W64 __w64
+#  else
+#     define _W64
+#  endif
+#endif
+
+
+// 7.18.1 Integer types
+
+// 7.18.1.1 Exact-width integer types
+
+// Visual Studio 6 and Embedded Visual C++ 4 doesn't
+// realize that, e.g. char has the same size as __int8
+// so we give up on __intX for them.
+#if (_MSC_VER < 1300)
+   typedef signed char       int8_t;
+   typedef signed short      int16_t;
+   typedef signed int        int32_t;
+   typedef unsigned char     uint8_t;
+   typedef unsigned short    uint16_t;
+   typedef unsigned int      uint32_t;
+#else
+   typedef signed __int8     int8_t;
+   typedef signed __int16    int16_t;
+   typedef signed __int32    int32_t;
+   typedef unsigned __int8   uint8_t;
+   typedef unsigned __int16  uint16_t;
+   typedef unsigned __int32  uint32_t;
+#endif
+typedef signed __int64       int64_t;
+typedef unsigned __int64     uint64_t;
+
+
+// 7.18.1.2 Minimum-width integer types
+typedef int8_t    int_least8_t;
+typedef int16_t   int_least16_t;
+typedef int32_t   int_least32_t;
+typedef int64_t   int_least64_t;
+typedef uint8_t   uint_least8_t;
+typedef uint16_t  uint_least16_t;
+typedef uint32_t  uint_least32_t;
+typedef uint64_t  uint_least64_t;
+
+// 7.18.1.3 Fastest minimum-width integer types
+typedef int8_t    int_fast8_t;
+typedef int16_t   int_fast16_t;
+typedef int32_t   int_fast32_t;
+typedef int64_t   int_fast64_t;
+typedef uint8_t   uint_fast8_t;
+typedef uint16_t  uint_fast16_t;
+typedef uint32_t  uint_fast32_t;
+typedef uint64_t  uint_fast64_t;
+
+// 7.18.1.4 Integer types capable of holding object pointers
+#ifdef _WIN64 // [
+   typedef signed __int64    intptr_t;
+   typedef unsigned __int64  uintptr_t;
+#else // _WIN64 ][
+   typedef _W64 signed int   intptr_t;
+   typedef _W64 unsigned int uintptr_t;
+#endif // _WIN64 ]
+
+// 7.18.1.5 Greatest-width integer types
+typedef int64_t   intmax_t;
+typedef uint64_t  uintmax_t;
+
+
+// 7.18.2 Limits of specified-width integer types
+
+#if !defined(__cplusplus) || defined(__STDC_LIMIT_MACROS) // [   See footnote 220 at page 257 and footnote 221 at page 259
+
+// 7.18.2.1 Limits of exact-width integer types
+#define INT8_MIN     ((int8_t)_I8_MIN)
+#define INT8_MAX     _I8_MAX
+#define INT16_MIN    ((int16_t)_I16_MIN)
+#define INT16_MAX    _I16_MAX
+#define INT32_MIN    ((int32_t)_I32_MIN)
+#define INT32_MAX    _I32_MAX
+#define INT64_MIN    ((int64_t)_I64_MIN)
+#define INT64_MAX    _I64_MAX
+#define UINT8_MAX    _UI8_MAX
+#define UINT16_MAX   _UI16_MAX
+#define UINT32_MAX   _UI32_MAX
+#define UINT64_MAX   _UI64_MAX
+
+// 7.18.2.2 Limits of minimum-width integer types
+#define INT_LEAST8_MIN    INT8_MIN
+#define INT_LEAST8_MAX    INT8_MAX
+#define INT_LEAST16_MIN   INT16_MIN
+#define INT_LEAST16_MAX   INT16_MAX
+#define INT_LEAST32_MIN   INT32_MIN
+#define INT_LEAST32_MAX   INT32_MAX
+#define INT_LEAST64_MIN   INT64_MIN
+#define INT_LEAST64_MAX   INT64_MAX
+#define UINT_LEAST8_MAX   UINT8_MAX
+#define UINT_LEAST16_MAX  UINT16_MAX
+#define UINT_LEAST32_MAX  UINT32_MAX
+#define UINT_LEAST64_MAX  UINT64_MAX
+
+// 7.18.2.3 Limits of fastest minimum-width integer types
+#define INT_FAST8_MIN    INT8_MIN
+#define INT_FAST8_MAX    INT8_MAX
+#define INT_FAST16_MIN   INT16_MIN
+#define INT_FAST16_MAX   INT16_MAX
+#define INT_FAST32_MIN   INT32_MIN
+#define INT_FAST32_MAX   INT32_MAX
+#define INT_FAST64_MIN   INT64_MIN
+#define INT_FAST64_MAX   INT64_MAX
+#define UINT_FAST8_MAX   UINT8_MAX
+#define UINT_FAST16_MAX  UINT16_MAX
+#define UINT_FAST32_MAX  UINT32_MAX
+#define UINT_FAST64_MAX  UINT64_MAX
+
+// 7.18.2.4 Limits of integer types capable of holding object pointers
+#ifdef _WIN64 // [
+#  define INTPTR_MIN   INT64_MIN
+#  define INTPTR_MAX   INT64_MAX
+#  define UINTPTR_MAX  UINT64_MAX
+#else // _WIN64 ][
+#  define INTPTR_MIN   INT32_MIN
+#  define INTPTR_MAX   INT32_MAX
+#  define UINTPTR_MAX  UINT32_MAX
+#endif // _WIN64 ]
+
+// 7.18.2.5 Limits of greatest-width integer types
+#define INTMAX_MIN   INT64_MIN
+#define INTMAX_MAX   INT64_MAX
+#define UINTMAX_MAX  UINT64_MAX
+
+// 7.18.3 Limits of other integer types
+
+#ifdef _WIN64 // [
+#  define PTRDIFF_MIN  _I64_MIN
+#  define PTRDIFF_MAX  _I64_MAX
+#else  // _WIN64 ][
+#  define PTRDIFF_MIN  _I32_MIN
+#  define PTRDIFF_MAX  _I32_MAX
+#endif  // _WIN64 ]
+
+#define SIG_ATOMIC_MIN  INT_MIN
+#define SIG_ATOMIC_MAX  INT_MAX
+
+#ifndef SIZE_MAX // [
+#  ifdef _WIN64 // [
+#     define SIZE_MAX  _UI64_MAX
+#  else // _WIN64 ][
+#     define SIZE_MAX  _UI32_MAX
+#  endif // _WIN64 ]
+#endif // SIZE_MAX ]
+
+// WCHAR_MIN and WCHAR_MAX are also defined in <wchar.h>
+#ifndef WCHAR_MIN // [
+#  define WCHAR_MIN  0
+#endif  // WCHAR_MIN ]
+#ifndef WCHAR_MAX // [
+#  define WCHAR_MAX  _UI16_MAX
+#endif  // WCHAR_MAX ]
+
+#define WINT_MIN  0
+#define WINT_MAX  _UI16_MAX
+
+#endif // __STDC_LIMIT_MACROS ]
+
+
+// 7.18.4 Limits of other integer types
+
+#if !defined(__cplusplus) || defined(__STDC_CONSTANT_MACROS) // [   See footnote 224 at page 260
+
+// 7.18.4.1 Macros for minimum-width integer constants
+
+#define INT8_C(val)  val##i8
+#define INT16_C(val) val##i16
+#define INT32_C(val) val##i32
+#define INT64_C(val) val##i64
+
+#define UINT8_C(val)  val##ui8
+#define UINT16_C(val) val##ui16
+#define UINT32_C(val) val##ui32
+#define UINT64_C(val) val##ui64
+
+// 7.18.4.2 Macros for greatest-width integer constants
+// These #ifndef's are needed to prevent collisions with <boost/cstdint.hpp>.
+// Check out Issue 9 for the details.
+#ifndef INTMAX_C //   [
+#  define INTMAX_C   INT64_C
+#endif // INTMAX_C    ]
+#ifndef UINTMAX_C //  [
+#  define UINTMAX_C  UINT64_C
+#endif // UINTMAX_C   ]
+
+#endif // __STDC_CONSTANT_MACROS ]
+
+#endif // _MSC_VER >= 1600 ]
+
+#endif // _MSC_STDINT_H_ ]
--- a/Resources/ThirdParty/base64/base64.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/ThirdParty/base64/base64.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,128 +1,128 @@
-/* 
-   base64.cpp and base64.h
-
-   Copyright (C) 2004-2008 René Nyffenegger
-
-   This source code is provided 'as-is', without any express or implied
-   warranty. In no event will the author 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 source code must not be misrepresented; you must not
-      claim that you wrote the original source code. If you use this source code
-      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 source code.
-
-   3. This notice may not be removed or altered from any source distribution.
-
-   René Nyffenegger rene.nyffenegger@adp-gmbh.ch
-
-*/
-
-#include "base64.h"
-#include <string.h>
-
-static const std::string base64_chars = 
-             "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-             "abcdefghijklmnopqrstuvwxyz"
-             "0123456789+/";
-
-
-static inline bool is_base64(unsigned char c) {
-  return (isalnum(c) || (c == '+') || (c == '/'));
-}
-
-std::string base64_encode(const std::string& stringToEncode) 
-{
-  const unsigned char* bytes_to_encode = reinterpret_cast<const unsigned char*>
-    (stringToEncode.size() > 0 ? &stringToEncode[0] : NULL);
-  unsigned int in_len = stringToEncode.size();
-  
-  std::string ret;
-  int i = 0;
-  int j = 0;
-  unsigned char char_array_3[3];
-  unsigned char char_array_4[4];
-
-  while (in_len--) {
-    char_array_3[i++] = *(bytes_to_encode++);
-    if (i == 3) {
-      char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
-      char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
-      char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
-      char_array_4[3] = char_array_3[2] & 0x3f;
-
-      for(i = 0; (i <4) ; i++)
-        ret += base64_chars[char_array_4[i]];
-      i = 0;
-    }
-  }
-
-  if (i)
-  {
-    for(j = i; j < 3; j++)
-      char_array_3[j] = '\0';
-
-    char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
-    char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
-    char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
-    char_array_4[3] = char_array_3[2] & 0x3f;
-
-    for (j = 0; (j < i + 1); j++)
-      ret += base64_chars[char_array_4[j]];
-
-    while((i++ < 3))
-      ret += '=';
-
-  }
-
-  return ret;
-}
-
-
-std::string base64_decode(const std::string& encoded_string) {
-  int in_len = encoded_string.size();
-  int i = 0;
-  int j = 0;
-  int in_ = 0;
-  unsigned char char_array_4[4], char_array_3[3];
-  std::string ret;
-
-  while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
-    char_array_4[i++] = encoded_string[in_]; in_++;
-    if (i ==4) {
-      for (i = 0; i <4; i++)
-        char_array_4[i] = base64_chars.find(char_array_4[i]);
-
-      char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
-      char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
-      char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
-
-      for (i = 0; (i < 3); i++)
-        ret += char_array_3[i];
-      i = 0;
-    }
-  }
-
-  if (i) {
-    for (j = i; j <4; j++)
-      char_array_4[j] = 0;
-
-    for (j = 0; j <4; j++)
-      char_array_4[j] = base64_chars.find(char_array_4[j]);
-
-    char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
-    char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
-    char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
-
-    for (j = 0; (j < i - 1); j++) ret += char_array_3[j];
-  }
-
-  return ret;
-}
+/* 
+   base64.cpp and base64.h
+
+   Copyright (C) 2004-2008 René Nyffenegger
+
+   This source code is provided 'as-is', without any express or implied
+   warranty. In no event will the author 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 source code must not be misrepresented; you must not
+      claim that you wrote the original source code. If you use this source code
+      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 source code.
+
+   3. This notice may not be removed or altered from any source distribution.
+
+   René Nyffenegger rene.nyffenegger@adp-gmbh.ch
+
+*/
+
+#include "base64.h"
+#include <string.h>
+
+static const std::string base64_chars = 
+             "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+             "abcdefghijklmnopqrstuvwxyz"
+             "0123456789+/";
+
+
+static inline bool is_base64(unsigned char c) {
+  return (isalnum(c) || (c == '+') || (c == '/'));
+}
+
+std::string base64_encode(const std::string& stringToEncode) 
+{
+  const unsigned char* bytes_to_encode = reinterpret_cast<const unsigned char*>
+    (stringToEncode.size() > 0 ? &stringToEncode[0] : NULL);
+  unsigned int in_len = stringToEncode.size();
+  
+  std::string ret;
+  int i = 0;
+  int j = 0;
+  unsigned char char_array_3[3];
+  unsigned char char_array_4[4];
+
+  while (in_len--) {
+    char_array_3[i++] = *(bytes_to_encode++);
+    if (i == 3) {
+      char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
+      char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
+      char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
+      char_array_4[3] = char_array_3[2] & 0x3f;
+
+      for(i = 0; (i <4) ; i++)
+        ret += base64_chars[char_array_4[i]];
+      i = 0;
+    }
+  }
+
+  if (i)
+  {
+    for(j = i; j < 3; j++)
+      char_array_3[j] = '\0';
+
+    char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
+    char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
+    char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
+    char_array_4[3] = char_array_3[2] & 0x3f;
+
+    for (j = 0; (j < i + 1); j++)
+      ret += base64_chars[char_array_4[j]];
+
+    while((i++ < 3))
+      ret += '=';
+
+  }
+
+  return ret;
+}
+
+
+std::string base64_decode(const std::string& encoded_string) {
+  int in_len = encoded_string.size();
+  int i = 0;
+  int j = 0;
+  int in_ = 0;
+  unsigned char char_array_4[4], char_array_3[3];
+  std::string ret;
+
+  while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
+    char_array_4[i++] = encoded_string[in_]; in_++;
+    if (i ==4) {
+      for (i = 0; i <4; i++)
+        char_array_4[i] = base64_chars.find(char_array_4[i]);
+
+      char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
+      char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
+      char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
+
+      for (i = 0; (i < 3); i++)
+        ret += char_array_3[i];
+      i = 0;
+    }
+  }
+
+  if (i) {
+    for (j = i; j <4; j++)
+      char_array_4[j] = 0;
+
+    for (j = 0; j <4; j++)
+      char_array_4[j] = base64_chars.find(char_array_4[j]);
+
+    char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
+    char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
+    char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
+
+    for (j = 0; (j < i - 1); j++) ret += char_array_3[j];
+  }
+
+  return ret;
+}
--- a/Resources/ThirdParty/base64/base64.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/ThirdParty/base64/base64.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,4 +1,4 @@
-#include <string>
-
-std::string base64_encode(const std::string& stringToEncode);
-std::string base64_decode(const std::string& s);
+#include <string>
+
+std::string base64_encode(const std::string& stringToEncode);
+std::string base64_decode(const std::string& s);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/WebAssembly/ArithmeticTests/CMakeLists.txt	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,101 @@
+# source ~/Downloads/emsdk-portable/emsdk_env.sh
+# cmake .. -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake \
+#       -DCMAKE_BUILD_TYPE=Debug \
+#       -DCMAKE_INSTALL_PREFIX=/tmp/wasm-install/
+# make install
+# -> Open the "/tmp/wasm-install/" with Firefox by serving it through Apache
+# -> Copy the result as "../arith.h"
+
+
+cmake_minimum_required(VERSION 2.8.3)
+
+
+#####################################################################
+## Configuration of the Emscripten compiler for WebAssembly target
+#####################################################################
+
+set(WASM_FLAGS "-s WASM=1 -s DISABLE_EXCEPTION_CATCHING=0")
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
+
+# Turn on support for debug exceptions
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DISABLE_EXCEPTION_CATCHING=0")
+
+
+#####################################################################
+## Prepare DCMTK 3.6.2
+#####################################################################
+
+set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../..)
+include(${ORTHANC_ROOT}/Resources/CMake/Compiler.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
+
+set(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.2)
+set(DCMTK_URL "http://www.orthanc-server.com/downloads/third-party/dcmtk-3.6.2.tar.gz")
+set(DCMTK_MD5 "d219a4152772985191c9b89d75302d12")
+
+if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}")
+  set(FirstRun OFF)
+else()
+  set(FirstRun ON)
+endif()
+
+DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}")
+
+if (FirstRun)
+  message("Patching file")
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+    ${CMAKE_SOURCE_DIR}/arith.patch
+    WORKING_DIRECTORY ${DCMTK_SOURCES_DIR}/config/tests
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+endif()
+
+
+#####################################################################
+## Build the DCMTK tests for arithmetics
+#####################################################################
+
+# https://github.com/kripken/emscripten/wiki/WebAssembly#web-server-setup
+file(WRITE ${CMAKE_BINARY_DIR}/.htaccess "
+AddType application/wasm .wasm
+AddOutputFilterByType DEFLATE application/wasm
+")
+
+file(WRITE ${CMAKE_BINARY_DIR}/dcmtk/config/osconfig.h "
+#pragma once
+#define HAVE_CMATH 1
+#define HAVE_MATH_H 1
+#define HAVE_PROTOTYPE_FINITE 1
+#define HAVE_PROTOTYPE_STD__ISINF 1
+#define HAVE_PROTOTYPE_STD__ISNAN 1
+#define HAVE_STD_NAMESPACE 1
+#define HAVE_STRSTREAM 1
+#define SIZEOF_VOID_P 4
+#define USE_STD_CXX_INCLUDES
+")
+
+include_directories(
+  ${DCMTK_SOURCES_DIR}/ofstd/include
+  ${CMAKE_BINARY_DIR}
+  )
+
+add_executable(dcmtk
+  ${DCMTK_SOURCES_DIR}/config/tests/arith.cc
+  ${CMAKE_SOURCE_DIR}/Run2.cpp
+  )
+
+install(TARGETS dcmtk DESTINATION .)
+
+install(FILES
+  ${CMAKE_BINARY_DIR}/.htaccess
+  ${CMAKE_BINARY_DIR}/dcmtk.wasm
+  ${CMAKE_SOURCE_DIR}/app.js
+  ${CMAKE_SOURCE_DIR}/index.html
+  DESTINATION .
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/WebAssembly/ArithmeticTests/Run2.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,22 @@
+#include <iostream>
+#include <emscripten/emscripten.h>
+
+extern "C"
+{
+  void EMSCRIPTEN_KEEPALIVE Run2()
+  {
+    // This stuff is not properly discovered by DCMTK 3.6.2 configuration scripts
+    std::cerr << std::endl << std::endl;
+    std::cerr << "/**" << std::endl;
+    std::cerr << "#define SIZEOF_CHAR " << sizeof(char) << std::endl;
+    std::cerr << "#define SIZEOF_DOUBLE " << sizeof(double) << std::endl;    
+    std::cerr << "#define SIZEOF_FLOAT " << sizeof(float) << std::endl;
+    std::cerr << "#define SIZEOF_INT " << sizeof(int) << std::endl;
+    std::cerr << "#define SIZEOF_LONG " << sizeof(long) << std::endl;
+    std::cerr << "#define SIZEOF_SHORT " << sizeof(short) << std::endl;
+    std::cerr << "#define SIZEOF_VOID_P " << sizeof(void*) << std::endl;
+    std::cerr << "#define C_CHAR_UNSIGNED " << (!std::is_signed<char>()) << std::endl;
+    std::cerr << "**/" << std::endl;
+    std::cerr << std::endl << std::endl;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/WebAssembly/ArithmeticTests/app.js	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,34 @@
+function Initialize()
+{
+  Module.ccall('Run2', // name of C function
+               null, // return type
+               [], // argument types
+               []);
+
+  Module.ccall('Run', // name of C function
+               'number', // return type
+               [], // argument types
+               []);
+}
+
+
+var Module = {
+  preRun: [],
+  postRun: [ Initialize ],
+  print: function(text) {
+    console.log(text);
+  },
+  printErr: function(text) {
+    if (text != 'Calling stub instead of signal()')
+    {
+      document.getElementById("stderr").textContent += text + '\n';
+    }
+  },
+  totalDependencies: 0
+};
+
+
+if (!('WebAssembly' in window)) {
+  alert('Sorry, your browser does not support WebAssembly :(');
+} else {
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/WebAssembly/ArithmeticTests/arith.patch	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,46 @@
+--- /home/jodogne/Subversion/orthanc/Resources/WebAssembly/ArithmeticTests/i/dcmtk-3.6.2/config/tests/arith.cc	2017-07-14 17:41:11.000000000 +0200
++++ arith.cc	2018-03-28 13:53:34.242234303 +0200
+@@ -19,6 +19,8 @@
+  *           for being used within oflimits.h.
+  */
+ 
++#include <emscripten/emscripten.h>
++
+ // Note: This depends on some files of ofstd and osconfig.h,
+ //       although it is part of configure testing itself.
+ //       Therefore, ensure osconfig.h has already been generated
+@@ -514,7 +516,9 @@
+ }
+ #endif
+ 
+-int main( int argc, char** argv )
++extern "C"
++{
++int EMSCRIPTEN_KEEPALIVE Run()
+ {
+ #ifdef HAVE_WINDOWS_H
+     // Activate the fallback workaround, it will only be used
+@@ -524,6 +528,8 @@
+ #endif
+ 
+     COUT << "Inspecting fundamental arithmetic types... " << OFendl;
++
++#if 0
+     if( argc != 2 )
+     {
+         STD_NAMESPACE cerr << "--   " << "Error: missing destination file "
+@@ -532,6 +538,9 @@
+     }
+ 
+     STD_NAMESPACE ofstream out( argv[1] );
++#else
++    std::ostream& out = std::cerr;
++#endif
+ 
+     out << "#ifndef CONFIG_ARITH_H" << '\n';
+     out << "#define CONFIG_ARITH_H" << '\n';
+@@ -619,3 +628,4 @@
+ 
+     return 0;
+ }
++}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/WebAssembly/ArithmeticTests/index.html	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en-us">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>DCMTK - Inspect arithmetic types</title>
+  </head>
+  <body>
+    <pre id="stderr"></pre>
+    <script type="text/javascript" src="app.js"></script>
+    <script type="text/javascript" async src="dcmtk.js"></script>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/WebAssembly/arith.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,70 @@
+/**
+#define SIZEOF_CHAR 1
+#define SIZEOF_DOUBLE 8
+#define SIZEOF_FLOAT 4
+#define SIZEOF_INT 4
+#define SIZEOF_LONG 4
+#define SIZEOF_SHORT 2
+#define SIZEOF_VOID_P 4
+#define C_CHAR_UNSIGNED 0
+**/
+
+
+#ifndef CONFIG_ARITH_H
+#define CONFIG_ARITH_H
+
+#define DCMTK_SIGNED_CHAR_DIGITS10 2
+#define DCMTK_UNSIGNED_CHAR_DIGITS10 2
+#define DCMTK_SIGNED_SHORT_DIGITS10 4
+#define DCMTK_UNSIGNED_SHORT_DIGITS10 4
+#define DCMTK_SIGNED_INT_DIGITS10 9
+#define DCMTK_UNSIGNED_INT_DIGITS10 9
+#define DCMTK_SIGNED_LONG_DIGITS10 9
+#define DCMTK_UNSIGNED_LONG_DIGITS10 9
+#define DCMTK_FLOAT_MAX_DIGITS10 9
+#define DCMTK_DOUBLE_MAX_DIGITS10 17
+#define DCMTK_CHAR_TRAPS OFFalse
+#define DCMTK_CHAR_MODULO OFTrue
+#define DCMTK_SIGNED_CHAR_TRAPS OFFalse
+#define DCMTK_SIGNED_CHAR_MODULO OFTrue
+#define DCMTK_UNSIGNED_CHAR_TRAPS OFFalse
+#define DCMTK_UNSIGNED_CHAR_MODULO OFTrue
+#define DCMTK_SIGNED_SHORT_TRAPS OFFalse
+#define DCMTK_SIGNED_SHORT_MODULO OFTrue
+#define DCMTK_UNSIGNED_SHORT_TRAPS OFFalse
+#define DCMTK_UNSIGNED_SHORT_MODULO OFTrue
+#define DCMTK_SIGNED_INT_TRAPS OFFalse
+#define DCMTK_SIGNED_INT_MODULO OFTrue
+#define DCMTK_UNSIGNED_INT_TRAPS OFFalse
+#define DCMTK_UNSIGNED_INT_MODULO OFTrue
+#define DCMTK_SIGNED_LONG_TRAPS OFFalse
+#define DCMTK_SIGNED_LONG_MODULO OFTrue
+#define DCMTK_UNSIGNED_LONG_TRAPS OFFalse
+#define DCMTK_UNSIGNED_LONG_MODULO OFTrue
+#define DCMTK_FLOAT_TRAPS OFFalse
+#define DCMTK_DOUBLE_TRAPS OFFalse
+#define DCMTK_FLOAT_HAS_INFINITY OFTrue
+#define DCMTK_FLOAT_INFINITY *OFreinterpret_cast( const float*, "\000\000\200\177" )
+#define DCMTK_DOUBLE_HAS_INFINITY OFTrue
+#define DCMTK_DOUBLE_INFINITY *OFreinterpret_cast( const double*, "\000\000\000\000\000\000\360\177" )
+#define DCMTK_FLOAT_HAS_QUIET_NAN OFTrue
+#define DCMTK_FLOAT_QUIET_NAN *OFreinterpret_cast( const float*, "\000\000\300\177" )
+#define DCMTK_DOUBLE_HAS_QUIET_NAN OFTrue
+#define DCMTK_DOUBLE_QUIET_NAN *OFreinterpret_cast( const double*, "\000\000\000\000\000\000\370\177" )
+#define DCMTK_FLOAT_HAS_SIGNALING_NAN OFFalse
+#define DCMTK_FLOAT_SIGNALING_NAN *OFreinterpret_cast( const float*, "\001\000\200\177" )
+#define DCMTK_DOUBLE_HAS_SIGNALING_NAN OFFalse
+#define DCMTK_DOUBLE_SIGNALING_NAN *OFreinterpret_cast( const double*, "\001\000\000\000\000\000\360\177" )
+#define DCMTK_FLOAT_IS_IEC559 OFFalse
+#define DCMTK_DOUBLE_IS_IEC559 OFFalse
+#define DCMTK_FLOAT_HAS_DENORM OFdenorm_present
+#define DCMTK_FLOAT_DENORM_MIN *OFreinterpret_cast( const float*, "\001\000\000\000" )
+#define DCMTK_DOUBLE_HAS_DENORM OFdenorm_present
+#define DCMTK_DOUBLE_DENORM_MIN *OFreinterpret_cast( const double*, "\001\000\000\000\000\000\000\000" )
+#define DCMTK_FLOAT_TINYNESS_BEFORE OFFalse
+#define DCMTK_DOUBLE_TINYNESS_BEFORE OFFalse
+#define DCMTK_FLOAT_HAS_DENORM_LOSS OFFalse
+#define DCMTK_DOUBLE_HAS_DENORM_LOSS OFFalse
+#define DCMTK_ROUND_STYLE 1
+
+#endif // CONFIG_ARITH_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/WebAssembly/dcdict.cc	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,1087 @@
+/*
+ *
+ *  Copyright (C) 1994-2016, OFFIS e.V.
+ *  All rights reserved.  See COPYRIGHT file for details.
+ *
+ *  This software and supporting documentation were developed by
+ *
+ *    OFFIS e.V.
+ *    R&D Division Health
+ *    Escherweg 2
+ *    D-26121 Oldenburg, Germany
+ *
+ *
+ *  Module:  dcmdata
+ *
+ *  Author:  Andrew Hewett
+ *
+ *  Purpose: loadable DICOM data dictionary
+ *
+ */
+
+
+#include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
+
+#include "dcmtk/ofstd/ofstd.h"
+#include "dcmtk/dcmdata/dcdict.h"
+#include "dcmtk/ofstd/ofdefine.h"
+#include "dcmtk/dcmdata/dcdicent.h"
+#include "dcmtk/dcmdata/dctypes.h"
+
+#define INCLUDE_CSTDLIB
+#define INCLUDE_CSTDIO
+#define INCLUDE_CSTRING
+#define INCLUDE_CCTYPE
+#include "dcmtk/ofstd/ofstdinc.h"
+
+/*
+** The separator character between fields in the data dictionary file(s)
+*/
+#define DCM_DICT_FIELD_SEPARATOR_CHAR '\t'
+
+/*
+** Comment character for the data dictionary file(s)
+*/
+#define DCM_DICT_COMMENT_CHAR '#'
+
+/*
+** THE Global DICOM Data Dictionary
+*/
+
+GlobalDcmDataDictionary dcmDataDict;
+
+
+/*
+** Member Functions
+*/
+
+static DcmDictEntry*
+makeSkelEntry(Uint16 group, Uint16 element,
+              Uint16 upperGroup, Uint16 upperElement,
+              DcmEVR evr, const char* tagName, int vmMin, int vmMax,
+              const char* standardVersion,
+              DcmDictRangeRestriction groupRestriction,
+              DcmDictRangeRestriction elementRestriction,
+              const char* privCreator)
+{
+    DcmDictEntry* e = NULL;
+    e = new DcmDictEntry(group, element, upperGroup, upperElement, evr,
+                         tagName, vmMin, vmMax, standardVersion, OFFalse, privCreator);
+    if (e != NULL) {
+        e->setGroupRangeRestriction(groupRestriction);
+        e->setElementRangeRestriction(elementRestriction);
+    }
+    return e;
+}
+
+
+OFBool DcmDataDictionary::loadSkeletonDictionary()
+{
+    /*
+    ** We need to know about Group Lengths to compute them
+    */
+    DcmDictEntry* e = NULL;
+    e = makeSkelEntry(0x0000, 0x0000, 0xffff, 0x0000,
+                      EVR_UL, "GenericGroupLength", 1, 1, "GENERIC",
+                      DcmDictRange_Unspecified, DcmDictRange_Unspecified, NULL);
+    addEntry(e);
+
+    /*
+    ** We need to know about Items and Delimitation Items to parse
+    ** (and construct) sequences.
+    */
+    e = makeSkelEntry(0xfffe, 0xe000, 0xfffe, 0xe000,
+                      EVR_na, "Item", 1, 1, "DICOM",
+                      DcmDictRange_Unspecified, DcmDictRange_Unspecified, NULL);
+    addEntry(e);
+    e = makeSkelEntry(0xfffe, 0xe00d, 0xfffe, 0xe00d,
+                      EVR_na, "ItemDelimitationItem", 1, 1, "DICOM",
+                      DcmDictRange_Unspecified, DcmDictRange_Unspecified, NULL);
+    addEntry(e);
+    e = makeSkelEntry(0xfffe, 0xe0dd, 0xfffe, 0xe0dd,
+                      EVR_na, "SequenceDelimitationItem", 1, 1, "DICOM",
+                      DcmDictRange_Unspecified, DcmDictRange_Unspecified, NULL);
+    addEntry(e);
+
+    skeletonCount = numberOfEntries();
+    return OFTrue;
+}
+
+
+DcmDataDictionary::DcmDataDictionary(OFBool loadBuiltin, OFBool loadExternal)
+  : hashDict(),
+    repDict(),
+    skeletonCount(0),
+    dictionaryLoaded(OFFalse)
+{
+    reloadDictionaries(loadBuiltin, loadExternal);
+}
+
+DcmDataDictionary::~DcmDataDictionary()
+{
+    clear();
+}
+
+
+void DcmDataDictionary::clear()
+{
+   hashDict.clear();
+   repDict.clear();
+   skeletonCount = 0;
+   dictionaryLoaded = OFFalse;
+}
+
+
+static void
+stripWhitespace(char* s)
+{
+  if (s)
+  {
+    unsigned char c;
+    unsigned char *t;
+    unsigned char *p;
+    t=p=OFreinterpret_cast(unsigned char *, s);
+    while ((c = *t++)) if (!isspace(c)) *p++ = c;
+    *p = '\0';
+  }
+}
+
+static char*
+stripTrailingWhitespace(char* s)
+{
+    if (s == NULL) return s;
+    for
+    (
+        char* it = s + strlen(s) - 1;
+        it >= s && isspace(OFstatic_cast(unsigned char, *it));
+        *it-- = '\0'
+    );
+    return s;
+}
+
+static void
+stripLeadingWhitespace(char* s)
+{
+  if (s)
+  {
+    unsigned char c;
+    unsigned char *t;
+    unsigned char *p;
+    t=p=OFreinterpret_cast(unsigned char *, s);
+    while (isspace(*t)) t++;
+    while ((c = *t++)) *p++ = c;
+    *p = '\0';
+  }
+}
+
+static OFBool
+parseVMField(char* vmField, int& vmMin, int& vmMax)
+{
+    OFBool ok = OFTrue;
+    char c = 0;
+    int dummy = 0;
+
+    /* strip any whitespace */
+    stripWhitespace(vmField);
+
+    if (sscanf(vmField, "%d-%d%c", &vmMin, &dummy, &c) == 3) {
+        /* treat "2-2n" like "2-n" for the moment */
+        if ((c == 'n') || (c == 'N')) {
+            vmMax = DcmVariableVM;
+        } else {
+            ok = OFFalse;
+        }
+    } else if (sscanf(vmField, "%d-%d", &vmMin, &vmMax) == 2) {
+        /* range VM (e.g. "2-6") */
+    } else if (sscanf(vmField, "%d-%c", &vmMin, &c) == 2) {
+        if ((c == 'n') || (c == 'N')) {
+            vmMax = DcmVariableVM;
+        } else {
+            ok = OFFalse;
+        }
+    } else if (sscanf(vmField, "%d%c", &vmMin, &c) == 2) {
+        /* treat "2n" like "2-n" for the moment */
+        if ((c == 'n') || (c == 'N')) {
+            vmMax = DcmVariableVM;
+        } else {
+            ok = OFFalse;
+        }
+    } else if (sscanf(vmField, "%d", &vmMin) == 1) {
+        /* fixed VM */
+        vmMax = vmMin;
+    } else if (sscanf(vmField, "%c", &c) == 1) {
+        /* treat "n" like "1-n" */
+        if ((c == 'n') || (c == 'N')) {
+            vmMin = 1;
+            vmMax = DcmVariableVM;
+        } else {
+            ok = OFFalse;
+        }
+    } else {
+        ok = OFFalse;
+    }
+    return ok;
+}
+
+static int
+splitFields(const char* line, char* fields[], int maxFields, char splitChar)
+{
+    const char *p;
+    int foundFields = 0;
+    size_t len;
+
+    do {
+#ifdef __BORLANDC__
+        // Borland Builder expects a non-const argument
+        p = strchr(OFconst_cast(char *, line), splitChar);
+#else
+        p = strchr(line, splitChar);
+#endif
+        if (p == NULL) {
+            len = strlen(line);
+        } else {
+            len = p - line;
+        }
+        fields[foundFields] = OFstatic_cast(char *, malloc(len + 1));
+        strncpy(fields[foundFields], line, len);
+        fields[foundFields][len] = '\0';
+        foundFields++;
+        line = p + 1;
+    } while ((foundFields < maxFields) && (p != NULL));
+
+    return foundFields;
+}
+
+static OFBool
+parseTagPart(char *s, unsigned int& l, unsigned int& h,
+             DcmDictRangeRestriction& r)
+{
+    OFBool ok = OFTrue;
+    char restrictor = ' ';
+
+    r = DcmDictRange_Unspecified; /* by default */
+
+    if (sscanf(s, "%x-%c-%x", &l, &restrictor, &h) == 3) {
+        switch (restrictor) {
+        case 'o':
+        case 'O':
+            r = DcmDictRange_Odd;
+            break;
+        case 'e':
+        case 'E':
+            r = DcmDictRange_Even;
+            break;
+        case 'u':
+        case 'U':
+            r = DcmDictRange_Unspecified;
+            break;
+        default:
+            DCMDATA_ERROR("DcmDataDictionary: Unknown range restrictor: " << restrictor);
+            ok = OFFalse;
+            break;
+        }
+    } else if (sscanf(s, "%x-%x", &l, &h) == 2) {
+        r = DcmDictRange_Even; /* by default */
+    } else if (sscanf(s, "%x", &l) == 1) {
+        h = l;
+    } else {
+        ok = OFFalse;
+    }
+    return ok;
+}
+
+static OFBool
+parseWholeTagField(char* s, DcmTagKey& key,
+                   DcmTagKey& upperKey,
+                   DcmDictRangeRestriction& groupRestriction,
+                   DcmDictRangeRestriction& elementRestriction,
+                   char *&privCreator)
+{
+    unsigned int gl, gh, el, eh;
+    groupRestriction = DcmDictRange_Unspecified;
+    elementRestriction = DcmDictRange_Unspecified;
+
+    stripLeadingWhitespace(s);
+    stripTrailingWhitespace(s);
+
+    char gs[64];
+    char es[64];
+    char pc[64];
+    size_t slen = strlen(s);
+
+    if (s[0] != '(') return OFFalse;
+    if (s[slen - 1] != ')') return OFFalse;
+    if (strchr(s, ',') == NULL) return OFFalse;
+
+    /* separate the group and element parts */
+    int i = 1; /* after the '(' */
+    int gi = 0;
+    for (; s[i] != ',' && s[i] != '\0'; i++)
+    {
+        gs[gi] = s[i];
+        gi++;
+    }
+    gs[gi] = '\0';
+
+    if (s[i] == '\0') return OFFalse; /* element part missing */
+    i++; /* after the ',' */
+
+    stripLeadingWhitespace(s + i);
+
+    int pi = 0;
+    if (s[i] == '\"') /* private creator */
+    {
+        i++;  // skip opening quotation mark
+        for (; s[i] != '\"' && s[i] != '\0'; i++) pc[pi++] = s[i];
+        pc[pi] = '\0';
+        if (s[i] == '\0') return OFFalse; /* closing quotation mark missing */
+        i++;
+        stripLeadingWhitespace(s + i);
+        if (s[i] != ',') return OFFalse; /* element part missing */
+        i++; /* after the ',' */
+    }
+
+    int ei = 0;
+    for (; s[i] != ')' && s[i] != '\0'; i++) {
+        es[ei] = s[i];
+        ei++;
+    }
+    es[ei] = '\0';
+
+    /* parse the tag parts into their components */
+    stripWhitespace(gs);
+    if (parseTagPart(gs, gl, gh, groupRestriction) == OFFalse)
+        return OFFalse;
+
+    stripWhitespace(es);
+    if (parseTagPart(es, el, eh, elementRestriction) == OFFalse)
+        return OFFalse;
+
+    if (pi > 0)
+    {
+      // copy private creator name
+      privCreator = new char[strlen(pc) + 1]; // deleted by caller
+      if (privCreator) strcpy(privCreator,pc);
+    }
+
+    key.set(OFstatic_cast(unsigned short, gl), OFstatic_cast(unsigned short, el));
+    upperKey.set(OFstatic_cast(unsigned short, gh), OFstatic_cast(unsigned short, eh));
+
+    return OFTrue;
+}
+
+static OFBool
+onlyWhitespace(const char* s)
+{
+    size_t len = strlen(s);
+    int charsFound = OFFalse;
+
+    for (size_t i = 0; (!charsFound) && (i < len); ++i) {
+        charsFound = !isspace(OFstatic_cast(unsigned char, s[i]));
+    }
+    return (!charsFound)? (OFTrue) : (OFFalse);
+}
+
+static char*
+getLine(char* line, int maxLineLen, FILE* f)
+{
+    char* s;
+
+    s = fgets(line, maxLineLen, f);
+
+    /* strip any trailing white space */
+    stripTrailingWhitespace(line);
+
+    return s;
+}
+
+static OFBool
+isaCommentLine(const char* s)
+{
+    OFBool isComment = OFFalse; /* assumption */
+    size_t len = strlen(s);
+    size_t i = 0;
+    for (i = 0; i < len && isspace(OFstatic_cast(unsigned char, s[i])); ++i) /*loop*/;
+    isComment = (s[i] == DCM_DICT_COMMENT_CHAR);
+    return isComment;
+}
+
+OFBool
+DcmDataDictionary::reloadDictionaries(OFBool loadBuiltin, OFBool loadExternal)
+{
+    OFBool result = OFTrue;
+    clear();
+    loadSkeletonDictionary();
+    if (loadBuiltin) {
+        loadBuiltinDictionary();
+        dictionaryLoaded = (numberOfEntries() > skeletonCount);
+        if (!dictionaryLoaded) result = OFFalse;
+    }
+    if (loadExternal) {
+        if (loadExternalDictionaries())
+            dictionaryLoaded = OFTrue;
+        else
+            result = OFFalse;
+    }
+    return result;
+}
+
+OFBool
+DcmDataDictionary::loadDictionary(const char* fileName, OFBool errorIfAbsent)
+{
+
+    char lineBuf[DCM_MAXDICTLINESIZE + 1];
+    FILE* f = NULL;
+    int lineNumber = 0;
+    char* lineFields[DCM_MAXDICTFIELDS + 1];
+    int fieldsPresent;
+    DcmDictEntry* e;
+    int errorsEncountered = 0;
+    OFBool errorOnThisLine = OFFalse;
+    int i;
+
+    DcmTagKey key, upperKey;
+    DcmDictRangeRestriction groupRestriction = DcmDictRange_Unspecified;
+    DcmDictRangeRestriction elementRestriction = DcmDictRange_Unspecified;
+    DcmVR vr;
+    char* vrName;
+    char* tagName;
+    char* privCreator;
+    int vmMin, vmMax = 1;
+    const char* standardVersion;
+
+    /* first, check whether 'fileName' really points to a file (and not to a directory or the like) */
+    if (!OFStandard::fileExists(fileName) || (f = fopen(fileName, "r")) == NULL) {
+        if (errorIfAbsent) {
+            DCMDATA_ERROR("DcmDataDictionary: Cannot open file: " << fileName);
+        }
+        return OFFalse;
+    }
+
+    DCMDATA_DEBUG("DcmDataDictionary: Loading file: " << fileName);
+
+    while (getLine(lineBuf, DCM_MAXDICTLINESIZE, f)) {
+        lineNumber++;
+
+        if (onlyWhitespace(lineBuf)) {
+            continue; /* ignore this line */
+        }
+        if (isaCommentLine(lineBuf)) {
+            continue; /* ignore this line */
+        }
+
+        errorOnThisLine = OFFalse;
+
+        /* fields are tab separated */
+        fieldsPresent = splitFields(lineBuf, lineFields,
+                                    DCM_MAXDICTFIELDS,
+                                    DCM_DICT_FIELD_SEPARATOR_CHAR);
+
+        /* initialize dict entry fields */
+        vrName = NULL;
+        tagName = NULL;
+        privCreator = NULL;
+        vmMin = vmMax = 1;
+        standardVersion = "DICOM";
+
+        switch (fieldsPresent) {
+        case 0:
+        case 1:
+        case 2:
+            DCMDATA_ERROR("DcmDataDictionary: "<< fileName << ": "
+                 << "too few fields (line " << lineNumber << ")");
+            errorOnThisLine = OFTrue;
+            break;
+        default:
+            DCMDATA_ERROR("DcmDataDictionary: " << fileName << ": "
+                 << "too many fields (line " << lineNumber << "): ");
+            errorOnThisLine = OFTrue;
+            break;
+        case 5:
+            stripWhitespace(lineFields[4]);
+            standardVersion = lineFields[4];
+            /* drop through to next case label */
+        case 4:
+            /* the VM field is present */
+            if (!parseVMField(lineFields[3], vmMin, vmMax)) {
+                DCMDATA_ERROR("DcmDataDictionary: " << fileName << ": "
+                     << "bad VM field (line " << lineNumber << "): " << lineFields[3]);
+                errorOnThisLine = OFTrue;
+            }
+            /* drop through to next case label */
+        case 3:
+            if (!parseWholeTagField(lineFields[0], key, upperKey,
+                 groupRestriction, elementRestriction, privCreator))
+            {
+                DCMDATA_ERROR("DcmDataDictionary: " << fileName << ": "
+                     << "bad Tag field (line " << lineNumber << "): " << lineFields[0]);
+                errorOnThisLine = OFTrue;
+            } else {
+                /* all is OK */
+                vrName = lineFields[1];
+                stripWhitespace(vrName);
+
+                tagName = lineFields[2];
+                stripWhitespace(tagName);
+            }
+        }
+
+        if (!errorOnThisLine) {
+            /* check the VR Field */
+            vr.setVR(vrName);
+            if (vr.getEVR() == EVR_UNKNOWN) {
+                DCMDATA_ERROR("DcmDataDictionary: " << fileName << ": "
+                     << "bad VR field (line " << lineNumber << "): " << vrName);
+                errorOnThisLine = OFTrue;
+            }
+        }
+
+        if (!errorOnThisLine) {
+            e = new DcmDictEntry(
+                key.getGroup(), key.getElement(),
+                upperKey.getGroup(), upperKey.getElement(),
+                vr, tagName, vmMin, vmMax, standardVersion, OFTrue,
+                privCreator);
+
+            e->setGroupRangeRestriction(groupRestriction);
+            e->setElementRangeRestriction(elementRestriction);
+            addEntry(e);
+        }
+
+        for (i = 0; i < fieldsPresent; i++) {
+            free(lineFields[i]);
+            lineFields[i] = NULL;
+        }
+
+        delete[] privCreator;
+
+        if (errorOnThisLine) {
+            errorsEncountered++;
+        }
+    }
+
+    fclose(f);
+
+    /* return OFFalse in case of errors and set internal state accordingly */
+    if (errorsEncountered == 0) {
+        dictionaryLoaded = OFTrue;
+        return OFTrue;
+    }
+    else {
+        dictionaryLoaded = OFFalse;
+        return OFFalse;
+    }
+}
+
+#ifndef HAVE_GETENV
+
+static
+char* getenv() {
+    return NULL;
+}
+
+#endif /* !HAVE_GETENV */
+
+
+
+OFBool
+DcmDataDictionary::loadExternalDictionaries()
+{
+    const char* env = NULL;
+    size_t len;
+    int sepCnt = 0;
+    OFBool msgIfDictAbsent = OFTrue;
+    OFBool loadFailed = OFFalse;
+
+    env = getenv(DCM_DICT_ENVIRONMENT_VARIABLE);
+    if ((env == NULL) || (strlen(env) == 0)) {
+        env = DCM_DICT_DEFAULT_PATH;
+        msgIfDictAbsent = OFFalse;
+    }
+
+    if ((env != NULL) && (strlen(env) != 0)) {
+        len = strlen(env);
+        for (size_t i = 0; i < len; ++i) {
+            if (env[i] == ENVIRONMENT_PATH_SEPARATOR) {
+                sepCnt++;
+            }
+        }
+
+        if (sepCnt == 0) {
+            if (!loadDictionary(env, msgIfDictAbsent)) {
+                return OFFalse;
+            }
+        } else {
+            char** dictArray;
+
+            dictArray = OFstatic_cast(char **, malloc((sepCnt + 1) * sizeof(char*)));
+
+            int ndicts = splitFields(env, dictArray, sepCnt + 1,
+                                     ENVIRONMENT_PATH_SEPARATOR);
+
+            for (int ii = 0; ii < ndicts; ii++) {
+                if ((dictArray[ii] != NULL) && (strlen(dictArray[ii]) > 0)) {
+                    if (!loadDictionary(dictArray[ii], msgIfDictAbsent)) {
+                        loadFailed = OFTrue;
+                    }
+                }
+                free(dictArray[ii]);
+            }
+            free(dictArray);
+        }
+    }
+
+    return (loadFailed) ? (OFFalse) : (OFTrue);
+}
+
+
+void
+DcmDataDictionary::addEntry(DcmDictEntry* e)
+{
+    if (e->isRepeating()) {
+        /*
+         * Find the best position in repeating tag list
+         * Existing entries are replaced if the ranges and repetition
+         * constraints are the same.
+         * If a range represents a subset of an existing range then it
+         * will be placed before it in the list.  This ensures that a
+         * search will find the subset rather than the superset.
+         * Otherwise entries are appended to the end of the list.
+         */
+        OFBool inserted = OFFalse;
+
+        DcmDictEntryListIterator iter(repDict.begin());
+        DcmDictEntryListIterator last(repDict.end());
+        for (; !inserted && iter != last; ++iter) {
+            if (e->setEQ(**iter)) {
+                /* replace the old entry with the new */
+                DcmDictEntry *old = *iter;
+                *iter = e;
+#ifdef PRINT_REPLACED_DICTIONARY_ENTRIES
+                DCMDATA_WARN("replacing " << *old);
+#endif
+                delete old;
+                inserted = OFTrue;
+            } else if (e->subset(**iter)) {
+                /* e is a subset of the current list position, insert before */
+                repDict.insert(iter, e);
+                inserted = OFTrue;
+            }
+        }
+        if (!inserted) {
+            /* insert at end */
+            repDict.push_back(e);
+            inserted = OFTrue;
+        }
+    } else {
+        hashDict.put(e);
+    }
+}
+
+void
+DcmDataDictionary::deleteEntry(const DcmDictEntry& entry)
+{
+    DcmDictEntry* e = NULL;
+    e = OFconst_cast(DcmDictEntry *, findEntry(entry));
+    if (e != NULL) {
+        if (e->isRepeating()) {
+            repDict.remove(e);
+            delete e;
+        } else {
+            hashDict.del(entry.getKey(), entry.getPrivateCreator());
+        }
+    }
+}
+
+const DcmDictEntry*
+DcmDataDictionary::findEntry(const DcmDictEntry& entry) const
+{
+    const DcmDictEntry* e = NULL;
+
+    if (entry.isRepeating()) {
+        OFBool found = OFFalse;
+        DcmDictEntryListConstIterator iter(repDict.begin());
+        DcmDictEntryListConstIterator last(repDict.end());
+        for (; !found && iter != last; ++iter) {
+            if (entry.setEQ(**iter)) {
+                found = OFTrue;
+                e = *iter;
+            }
+        }
+    } else {
+        e = hashDict.get(entry, entry.getPrivateCreator());
+    }
+    return e;
+}
+
+const DcmDictEntry*
+DcmDataDictionary::findEntry(const DcmTagKey& key, const char *privCreator) const
+{
+    /* search first in the normal tags dictionary and if not found
+     * then search in the repeating tags list.
+     */
+    const DcmDictEntry* e = NULL;
+
+    e = hashDict.get(key, privCreator);
+    if (e == NULL) {
+        /* search in the repeating tags dictionary */
+        OFBool found = OFFalse;
+        DcmDictEntryListConstIterator iter(repDict.begin());
+        DcmDictEntryListConstIterator last(repDict.end());
+        for (; !found && iter != last; ++iter) {
+            if ((*iter)->contains(key, privCreator)) {
+                found = OFTrue;
+                e = *iter;
+            }
+        }
+    }
+    return e;
+}
+
+const DcmDictEntry*
+DcmDataDictionary::findEntry(const char *name) const
+{
+    const DcmDictEntry* e = NULL;
+    const DcmDictEntry* ePrivate = NULL;
+
+    /* search first in the normal tags dictionary and if not found
+     * then search in the repeating tags list.
+     */
+    DcmHashDictIterator iter;
+    for (iter = hashDict.begin(); (e == NULL) && (iter != hashDict.end()); ++iter) {
+        if ((*iter)->contains(name)) {
+            e = *iter;
+            if (e->getGroup() % 2)
+            {
+                /* tag is a private tag - continue search to be sure to find non-private keys first */
+                if (!ePrivate) ePrivate = e;
+                e = NULL;
+            }
+        }
+    }
+
+    if (e == NULL) {
+        /* search in the repeating tags dictionary */
+        OFBool found = OFFalse;
+        DcmDictEntryListConstIterator iter2(repDict.begin());
+        DcmDictEntryListConstIterator last(repDict.end());
+        for (; !found && iter2 != last; ++iter2) {
+            if ((*iter2)->contains(name)) {
+                found = OFTrue;
+                e = *iter2;
+            }
+        }
+    }
+
+    if (e == NULL && ePrivate != NULL) {
+        /* no standard key found - use the first private key found */
+        e = ePrivate;
+    }
+
+    return e;
+}
+
+
+/* ================================================================== */
+
+
+GlobalDcmDataDictionary::GlobalDcmDataDictionary()
+  : dataDict(NULL)
+#ifdef WITH_THREADS
+  , dataDictLock()
+#endif
+{
+}
+
+GlobalDcmDataDictionary::~GlobalDcmDataDictionary()
+{
+  /* No threads may be active any more, so no locking needed */
+  delete dataDict;
+}
+
+void GlobalDcmDataDictionary::createDataDict()
+{
+  /* Make sure only one thread tries to initialize the dictionary */
+#ifdef WITH_THREADS
+  dataDictLock.wrlock();
+#endif
+#ifdef DONT_LOAD_EXTERNAL_DICTIONARIES
+  const OFBool loadExternal = OFFalse;
+#else
+  const OFBool loadExternal = OFTrue;
+#endif
+  /* Make sure no other thread managed to create the dictionary
+   * before we got our write lock. */
+  if (!dataDict)
+    dataDict = new DcmDataDictionary(OFTrue /*loadBuiltin*/, loadExternal);
+#ifdef WITH_THREADS
+  dataDictLock.unlock();
+#endif
+}
+
+const DcmDataDictionary& GlobalDcmDataDictionary::rdlock()
+{
+#ifdef WITH_THREADS
+  dataDictLock.rdlock();
+#endif
+  if (!dataDict)
+  {
+    /* dataDictLock must not be locked during createDataDict() */
+#ifdef WITH_THREADS
+    dataDictLock.unlock();
+#endif
+    createDataDict();
+#ifdef WITH_THREADS
+    dataDictLock.rdlock();
+#endif
+  }
+  return *dataDict;
+}
+
+DcmDataDictionary& GlobalDcmDataDictionary::wrlock()
+{
+#ifdef WITH_THREADS
+  dataDictLock.wrlock();
+#endif
+  if (!dataDict)
+  {
+    /* dataDictLock must not be locked during createDataDict() */
+#ifdef WITH_THREADS
+    dataDictLock.unlock();
+#endif
+    createDataDict();
+#ifdef WITH_THREADS
+    dataDictLock.wrlock();
+#endif
+  }
+  return *dataDict;
+}
+
+void GlobalDcmDataDictionary::unlock()
+{
+#ifdef WITH_THREADS
+  dataDictLock.unlock();
+#endif
+}
+
+OFBool GlobalDcmDataDictionary::isDictionaryLoaded()
+{
+  OFBool result = rdlock().isDictionaryLoaded();
+  unlock();
+  return result;
+}
+
+void GlobalDcmDataDictionary::clear()
+{
+  wrlock().clear();
+  unlock();
+}
+
+
+
+
+// Function by the Orthanc project to load a dictionary from a memory
+// buffer, which is necessary in sandboxed environments. This is an
+// adapted version of DcmDataDictionary::loadDictionary().
+
+
+#include <boost/noncopyable.hpp>
+
+struct OrthancLinesIterator;
+
+// This plain old C class is implemented in "../../Core/Toolbox.h"
+OrthancLinesIterator* OrthancLinesIterator_Create(const std::string& content);
+
+bool OrthancLinesIterator_GetLine(std::string& target,
+                                  const OrthancLinesIterator* iterator);
+
+void OrthancLinesIterator_Next(OrthancLinesIterator* iterator);
+
+void OrthancLinesIterator_Free(OrthancLinesIterator* iterator);
+
+
+class LinesIterator : public boost::noncopyable
+{
+private:
+  OrthancLinesIterator* iterator_;
+  
+public:
+  LinesIterator(const std::string& content) :
+    iterator_(NULL)
+  {
+    iterator_ = OrthancLinesIterator_Create(content);
+  }
+
+  ~LinesIterator()
+  {
+    if (iterator_ != NULL)
+    {
+      OrthancLinesIterator_Free(iterator_);
+      iterator_ = NULL;
+    }
+  }
+  
+  bool GetLine(std::string& target) const
+  {
+    if (iterator_ != NULL)
+    {
+      return OrthancLinesIterator_GetLine(target, iterator_);
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  void Next()
+  {
+    if (iterator_ != NULL)
+    {
+      OrthancLinesIterator_Next(iterator_);
+    }
+  }
+};
+
+
+
+OFBool
+DcmDataDictionary::loadFromMemory(const std::string& content, OFBool errorIfAbsent)
+{
+  int lineNumber = 0;
+  char* lineFields[DCM_MAXDICTFIELDS + 1];
+  int fieldsPresent;
+  DcmDictEntry* e;
+  int errorsEncountered = 0;
+  OFBool errorOnThisLine = OFFalse;
+  int i;
+
+  DcmTagKey key, upperKey;
+  DcmDictRangeRestriction groupRestriction = DcmDictRange_Unspecified;
+  DcmDictRangeRestriction elementRestriction = DcmDictRange_Unspecified;
+  DcmVR vr;
+  char* vrName;
+  char* tagName;
+  char* privCreator;
+  int vmMin, vmMax = 1;
+  const char* standardVersion;
+
+  LinesIterator iterator(content);
+
+  std::string line;
+  while (iterator.GetLine(line)) {
+    iterator.Next();
+
+    if (line.size() >= DCM_MAXDICTLINESIZE) {
+      DCMDATA_ERROR("DcmDataDictionary: Too long line: " << line);
+      continue;
+    }
+
+    lineNumber++;
+
+    if (onlyWhitespace(line.c_str())) {
+      continue; /* ignore this line */
+    }
+    if (isaCommentLine(line.c_str())) {
+      continue; /* ignore this line */
+    }
+
+    errorOnThisLine = OFFalse;
+
+    /* fields are tab separated */
+    fieldsPresent = splitFields(line.c_str(), lineFields,
+                                DCM_MAXDICTFIELDS,
+                                DCM_DICT_FIELD_SEPARATOR_CHAR);
+
+    /* initialize dict entry fields */
+    vrName = NULL;
+    tagName = NULL;
+    privCreator = NULL;
+    vmMin = vmMax = 1;
+    standardVersion = "DICOM";
+
+    switch (fieldsPresent) {
+      case 0:
+      case 1:
+      case 2:
+        DCMDATA_ERROR("DcmDataDictionary: "
+                      << "too few fields (line " << lineNumber << ")");
+        errorOnThisLine = OFTrue;
+        break;
+      default:
+        DCMDATA_ERROR("DcmDataDictionary: "
+                      << "too many fields (line " << lineNumber << "): ");
+        errorOnThisLine = OFTrue;
+        break;
+      case 5:
+        stripWhitespace(lineFields[4]);
+        standardVersion = lineFields[4];
+        /* drop through to next case label */
+      case 4:
+        /* the VM field is present */
+        if (!parseVMField(lineFields[3], vmMin, vmMax)) {
+          DCMDATA_ERROR("DcmDataDictionary: "
+                        << "bad VM field (line " << lineNumber << "): " << lineFields[3]);
+          errorOnThisLine = OFTrue;
+        }
+        /* drop through to next case label */
+      case 3:
+        if (!parseWholeTagField(lineFields[0], key, upperKey,
+                                groupRestriction, elementRestriction, privCreator))
+        {
+          DCMDATA_ERROR("DcmDataDictionary: "
+                        << "bad Tag field (line " << lineNumber << "): " << lineFields[0]);
+          errorOnThisLine = OFTrue;
+        } else {
+          /* all is OK */
+          vrName = lineFields[1];
+          stripWhitespace(vrName);
+
+          tagName = lineFields[2];
+          stripWhitespace(tagName);
+        }
+    }
+
+    if (!errorOnThisLine) {
+      /* check the VR Field */
+      vr.setVR(vrName);
+      if (vr.getEVR() == EVR_UNKNOWN) {
+        DCMDATA_ERROR("DcmDataDictionary: "
+                      << "bad VR field (line " << lineNumber << "): " << vrName);
+        errorOnThisLine = OFTrue;
+      }
+    }
+
+    if (!errorOnThisLine) {
+      e = new DcmDictEntry(
+        key.getGroup(), key.getElement(),
+        upperKey.getGroup(), upperKey.getElement(),
+        vr, tagName, vmMin, vmMax, standardVersion, OFTrue,
+        privCreator);
+
+      e->setGroupRangeRestriction(groupRestriction);
+      e->setElementRangeRestriction(elementRestriction);
+      addEntry(e);
+    }
+
+    for (i = 0; i < fieldsPresent; i++) {
+      free(lineFields[i]);
+      lineFields[i] = NULL;
+    }
+
+    delete[] privCreator;
+
+    if (errorOnThisLine) {
+      errorsEncountered++;
+    }
+  }
+
+  /* return OFFalse in case of errors and set internal state accordingly */
+  if (errorsEncountered == 0) {
+    dictionaryLoaded = OFTrue;
+    return OFTrue;
+  }
+  else {
+    dictionaryLoaded = OFFalse;
+    return OFFalse;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/WebAssembly/dcdict.h	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,302 @@
+/*
+ *
+ *  Copyright (C) 1994-2015, OFFIS e.V.
+ *  All rights reserved.  See COPYRIGHT file for details.
+ *
+ *  This software and supporting documentation were developed by
+ *
+ *    OFFIS e.V.
+ *    R&D Division Health
+ *    Escherweg 2
+ *    D-26121 Oldenburg, Germany
+ *
+ *
+ *  Module:  dcmdata
+ *
+ *  Author:  Andrew Hewett
+ *
+ *  Purpose: Interface for loadable DICOM data dictionary
+ *
+ */
+
+
+#ifndef DCMDICT_H
+#define DCMDICT_H
+
+#include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
+
+#include "dcmtk/ofstd/ofthread.h"
+#include "dcmtk/dcmdata/dchashdi.h"
+
+/// maximum length of a line in the loadable DICOM dictionary
+#define DCM_MAXDICTLINESIZE     2048
+
+/// maximum number of fields per entry in the loadable DICOM dictionary
+#define DCM_MAXDICTFIELDS       6
+
+/// environment variable pointing to the data dictionary file
+#define DCM_DICT_ENVIRONMENT_VARIABLE   "DCMDICTPATH"
+
+#ifndef DCM_DICT_DEFAULT_PATH
+/*
+** The default dictionary path is system dependent.  It should
+** be defined in a configuration file included from "osconfig.h"
+*/
+#error "DCM_DICT_DEFAULT_PATH is not defined via osconfig.h"
+#endif /* !DCM_DICT_DEFAULT_PATH */
+
+#ifndef ENVIRONMENT_PATH_SEPARATOR
+#define ENVIRONMENT_PATH_SEPARATOR '\n' /* at least define something unlikely */
+#endif
+
+
+/** this class implements a loadable DICOM Data Dictionary
+ */
+class DCMTK_DCMDATA_EXPORT DcmDataDictionary
+{
+public:
+
+    /** constructor
+     *  @param loadBuiltin flag indicating if a built-in data dictionary
+     *    (if any) should be loaded.
+     *  @param loadExternal flag indicating if an external data dictionary
+     *    should be read from file.
+     */
+    DcmDataDictionary(OFBool loadBuiltin, OFBool loadExternal);
+
+    /// destructor
+    ~DcmDataDictionary();
+
+    /** checks if a data dictionary is loaded (excluding the skeleton dictionary)
+     *  @return true if loaded, false if no dictionary is present
+     */
+    OFBool isDictionaryLoaded() const { return dictionaryLoaded; }
+
+    /// returns the number of normal (non-repeating) tag entries
+    int numberOfNormalTagEntries() const { return hashDict.size(); }
+
+    /// returns the number of repeating tag entries
+    int numberOfRepeatingTagEntries() const { return OFstatic_cast(int, repDict.size()); }
+
+    /** returns the number of dictionary entries that were loaded
+     *  either from file or from a built-in dictionary or both.
+     */
+    int numberOfEntries() const
+        { return numberOfNormalTagEntries()
+              + numberOfRepeatingTagEntries() - skeletonCount; }
+
+    /** returns the number of skeleton entries. The skeleton is a collection
+     *  of dictionary entries which are always present, even if neither internal
+     *  nor external dictionary have been loaded. It contains very basic
+     *  things like item delimitation and sequence delimitation.
+     */
+    int numberOfSkeletonEntries() const { return skeletonCount; }
+
+    /** reload data dictionaries. First, all dictionary entries are deleted.
+     *  @param loadBuiltin flag indicating if a built-in data dictionary
+     *    (if any) should be loaded.
+     *  @param loadExternal flag indicating if an external data dictionary
+     *    should be read from file.
+     *  @return true if reload was successful, false if an error occurred
+     */
+    OFBool reloadDictionaries(OFBool loadBuiltin, OFBool loadExternal);
+
+    /** load a particular dictionary from file.
+     *  @param fileName filename
+     *  @param errorIfAbsent causes the method to return false
+     *     if the file cannot be opened
+     *  @return false if the file contains a parse error or if the file could
+     *     not be opened and errorIfAbsent was set, true otherwise.
+     */
+    OFBool loadDictionary(const char* fileName, OFBool errorIfAbsent = OFTrue);
+
+    /** dictionary lookup for the given tag key and private creator name.
+     *  First the normal tag dictionary is searched.  If not found
+     *  then the repeating tag dictionary is searched.
+     *  @param key tag key
+     *  @param privCreator private creator name, may be NULL
+     */
+    const DcmDictEntry* findEntry(const DcmTagKey& key, const char *privCreator) const;
+
+    /** dictionary lookup for the given attribute name.
+     *  First the normal tag dictionary is searched.  If not found
+     *  then the repeating tag dictionary is searched.
+     *  Only considers standard attributes (i. e. without private creator)
+     *  @param name attribute name
+     */
+    const DcmDictEntry* findEntry(const char *name) const;
+
+    /// deletes all dictionary entries
+    void clear();
+
+    /** adds an entry to the dictionary.  Must be allocated via new.
+     *  The entry becomes the property of the dictionary and will be
+     *  deallocated (via delete) upon clear() or dictionary destruction.
+     *  If an equivalent entry already exists it will be replaced by
+     *  the new entry and the old entry deallocated (via delete).
+     *  @param entry pointer to new entry
+     */
+    void addEntry(DcmDictEntry* entry);
+
+    /* Iterators to access the normal and the repeating entries */
+
+    /// returns an iterator to the start of the normal (non-repeating) dictionary
+    DcmHashDictIterator normalBegin() { return hashDict.begin(); }
+
+    /// returns an iterator to the end of the normal (non-repeating) dictionary
+    DcmHashDictIterator normalEnd() { return hashDict.end(); }
+
+    /// returns an iterator to the start of the repeating tag dictionary
+    DcmDictEntryListIterator repeatingBegin() { return repDict.begin(); }
+
+    /// returns an iterator to the end of the repeating tag dictionary
+    DcmDictEntryListIterator repeatingEnd() { return repDict.end(); }
+
+    // Function by the Orthanc project to load a dictionary from a
+    // memory buffer, which is necessary in sandboxed
+    // environments. This is an adapted version of
+    // DcmDataDictionary::loadDictionary().
+    OFBool loadFromMemory(const std::string& content, OFBool errorIfAbsent = OFTrue);
+
+private:
+
+    /** private undefined assignment operator
+     */
+    DcmDataDictionary &operator=(const DcmDataDictionary &);
+
+    /** private undefined copy constructor
+     */
+    DcmDataDictionary(const DcmDataDictionary &);
+
+    /** loads external dictionaries defined via environment variables
+     *  @return true if successful
+     */
+    OFBool loadExternalDictionaries();
+
+    /** loads a builtin (compiled) data dictionary.
+     *  Depending on which code is in use, this function may not
+     *  do anything.
+     */
+    void loadBuiltinDictionary();
+
+    /** loads the skeleton dictionary (the bare minimum needed to run)
+     *  @return true if successful
+     */
+    OFBool loadSkeletonDictionary();
+
+    /** looks up the given directory entry in the two dictionaries.
+     *  @return pointer to entry if found, NULL otherwise
+     */
+    const DcmDictEntry* findEntry(const DcmDictEntry& entry) const;
+
+    /** deletes the given entry from either dictionary
+     */
+    void deleteEntry(const DcmDictEntry& entry);
+
+
+    /** dictionary of normal tags
+     */
+    DcmHashDict hashDict;
+
+    /** dictionary of repeating tags
+     */
+    DcmDictEntryList repDict;
+
+    /** the number of skeleton entries
+     */
+    int skeletonCount;
+
+    /** is a dictionary loaded (more than skeleton)
+     */
+    OFBool dictionaryLoaded;
+
+};
+
+
+/** global singleton dicom dictionary that is used by DCMTK in order to lookup
+ *  attribute VR, tag names and so on.  The dictionary is internally populated
+ *  on first use, if the user accesses it via rdlock() or wrlock().  The
+ *  dictionary allows safe read (shared) and write (exclusive) access from
+ *  multiple threads in parallel.
+ */
+class DCMTK_DCMDATA_EXPORT GlobalDcmDataDictionary
+{
+public:
+  /** constructor.
+   */
+  GlobalDcmDataDictionary();
+
+  /** destructor
+   */
+  ~GlobalDcmDataDictionary();
+
+  /** acquires a read lock and returns a const reference to
+   *  the dictionary.
+   *  @return const reference to dictionary
+   */
+  const DcmDataDictionary& rdlock();
+
+  /** acquires a write lock and returns a non-const reference
+   *  to the dictionary.
+   *  @return non-const reference to dictionary.
+   */
+  DcmDataDictionary& wrlock();
+
+  /** unlocks the read or write lock which must have been acquired previously.
+   */
+  void unlock();
+
+  /** checks if a data dictionary has been loaded. This method acquires and
+   *  releases a read lock. It must not be called with another lock on the
+   *  dictionary being held by the calling thread.
+   *  @return OFTrue if dictionary has been loaded, OFFalse otherwise.
+   */
+  OFBool isDictionaryLoaded();
+
+  /** erases the contents of the dictionary. This method acquires and
+   *  releases a write lock. It must not be called with another lock on the
+   *  dictionary being held by the calling thread.  This method is intended
+   *  as a help for debugging memory leaks.
+   */
+  void clear();
+
+private:
+  /** private undefined assignment operator
+   */
+  GlobalDcmDataDictionary &operator=(const GlobalDcmDataDictionary &);
+
+  /** private undefined copy constructor
+   */
+  GlobalDcmDataDictionary(const GlobalDcmDataDictionary &);
+
+  /** create the data dictionary instance for this class. Used for first
+   * intialization.  The caller must not have dataDictLock locked.
+   */
+  void createDataDict();
+
+  /** the data dictionary managed by this class
+   */
+  DcmDataDictionary *dataDict;
+
+#ifdef WITH_THREADS
+  /** the read/write lock used to protect access from multiple threads
+   */
+  OFReadWriteLock dataDictLock;
+#endif
+};
+
+
+/** The Global DICOM Data Dictionary.
+ *  Will be created before main() starts and gets populated on its first use.
+ *  Tries to load a builtin data dictionary (if compiled in).
+ *  Tries to load data dictionaries from files specified by
+ *  the DCMDICTPATH environment variable.  If this environment
+ *  variable does not exist then a default file is loaded (if
+ *  it exists).
+ *  It is possible that no data dictionary gets loaded.  This
+ *  is likely to cause unexpected behaviour in the dcmdata
+ *  toolkit classes.
+ */
+extern DCMTK_DCMDATA_EXPORT GlobalDcmDataDictionary dcmDataDict;
+
+#endif
--- a/Resources/WindowsResources.py	Thu Oct 29 11:25:45 2015 +0100
+++ b/Resources/WindowsResources.py	Fri Oct 12 15:18:10 2018 +0200
@@ -1,8 +1,9 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/THANKS	Thu Oct 29 11:25:45 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-Orthanc - A Lightweight, RESTful DICOM Server
-=============================================
-
-Orthanc has originally been written by Sebastien Jodogne
-(cf. "AUTHORS" file). This file contains the list of the people that
-have further contributed to Orthanc by reporting problems, suggesting
-various improvements, or submitting actual code. Please help keep it
-complete and exempt or errors.
-
-
-Contributors
-------------
-
-* Cyril Paulus <cyril.paulus@student.ulg.ac.be>, for the build process
-  and suggestions about the REST API.
-* Will Ryder <will.ryder@sydney.edu.au>, for improvements with the
-  handling of series with temporal positions (fMRI and dynamic PET).
-* Ryan Walklin <ryanwalklin@gmail.com>, for Mac OS X build.
-* Peter Somlo <peter.somlo@gmail.com>, for ClearCanvas and JPEG support.
-* 12maksqwe@gmail.com, for fixing issue #8.
-* Julien Nabet, for various suggestions to improve the source code.
-* Karsten Hilbert <Karsten.Hilbert@gmx.net>, for in-depth testing.
-* Marek Swiecicki <mswiecicki@archimedic.pl>, for various suggestions
-  and sample DICOM files.
-* Chris Hafey <chafey@gmail.com>, for suggesting many features and
-  improvements, for a Windows service+installer with .NET/NSIS.
-* Manabu Tokunaga <manabu@lury.net>, for a Windows service with .NET.
-* Vincent Kersten <vincent1234567@gmail.com>, for DICOMDIR in the GUI.
-* Emsy Chan <emlscs@yahoo.com>, for various contributions
-  and sample DICOM files.
-
-
-Thanks also to all the contributors active in our Google Group:
-https://groups.google.com/forum/#!forum/orthanc-users
-
-
-Debian/Ubuntu
--------------
-
-* Mathieu Malaterre <mathieu.malaterre@gmail.com>, for sponsoring Orthanc.
-* Andreas Tille <andreas@an3as.eu>, for help about packaging. 
-* Adam Conrad <adconrad@debian.org>, to improve support of big endianness.
-
-
-Fedora and Red Hat
-------------------
-
-* Mario Ceresa <mrceresa@gmail.com>, for help about packaging.
-
-
-FreeBSD
--------
-
-* Mikhail <mp39590@gmail.com>, for FreeBSD packaging.
-
-
-Artwork
--------
-
-https://code.google.com/p/orthanc/wiki/Logos
-
-* Benjamin Golinvaux <golinvauxb@gmail.com>, for creating the official logo.
-* Jean-François Degbomont <jfdegbo@gmail.com>, for submitting a logo.
-* Martin Jolly <martin.jolly@gmail.com>, for submitting a logo.
-* Philippe Sepers <sepers.philippe@gmail.com>, for submitting a logo.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/TODO	Fri Oct 12 15:18:10 2018 +0200
@@ -0,0 +1,170 @@
+=======================
+=== Orthanc Roadmap ===
+=======================
+
+The wishlist from Orthanc users is available on Trello:
+https://trello.com/b/gcn33tDM/orthanc-wishlist
+
+
+=======
+General
+=======
+
+* Configure an user-defined site UID root if generating DICOM UIDs
+  ("FromDcmtkBridge::GenerateUuid()")
+* Support "/preview" and "/matlab" for LUT color images
+* Improve handling of errors in the command queue:
+  https://groups.google.com/d/msg/orthanc-users/--njEbqcDDI/rBu8XL-Mm-cJ
+* Support partial file retrieval in Orthanc::HttpClient
+* Support retry counter in Orthanc::HttpClient
+* Option to enable DNS lookups in DICOM: https://goo.gl/woa35Z
+
+
+============
+Dependencies
+============
+
+* Switch from libiconv to libICU (http://site.icu-project.org/download),
+  as recommended by Boost:
+  http://www.boost.org/doc/libs/1_64_0/libs/locale/doc/html/building_boost_locale.html
+
+
+=============
+Documentation
+=============
+
+* Document REST API with Swagger/OpenAPI
+
+
+========
+REST API
+========
+
+----------
+Short-term
+----------
+
+* Create multi-frame images with /tools/create-dicom (by adding a
+  "MultiFrame" flag to avoid creating a series)
+* In the /studies/{id}/anonymize route, add an option to remove secondary captures.
+  They usually contains Patient info in the image.  The SOPClassUID might be used to
+  identify such secondary captures.
+
+---------
+Long-term
+---------
+
+* Stick to the JSONapi or JAREST guidelines for a "v2" of the API:
+  https://groups.google.com/forum/#!msg/orthanc-users/Bag-SwEE9ZI/-w7QXI6p7-oJ
+  http://www.admiraalit.nl/jarest/
+
+
+=====
+DICOM
+=====
+
+----------
+Short-term
+----------
+
+* Support C-GET: 
+  http://dclunie.blogspot.be/2016/05/to-c-move-is-human-to-c-get-divine.html
+* Check Big Endian transfer syntax in ParsedDicomFile::EmbedImage and
+  DicomImageDecoder
+
+---------
+Long-term
+---------
+
+* Support DICOM TLS (cf. "--enable-tls" in storescp)
+* Support Storage Commitment:
+  https://groups.google.com/forum/#!msg/orthanc-users/VZOn8St65jw/s8kg_OHesj0J
+* Support extended association:
+  https://groups.google.com/d/msg/orthanc-users/xD4d3mpc6ms/srF7E2goAAAJ
+
+
+=======
+Plugins
+=======
+
+---
+SDK
+---
+
+* Image transcoding API
+* Add plugins for normalized operations (notably so as to support
+  Print SCU/SCP):
+  https://www.medicalconnections.co.uk/kb/DICOM_Print_Service
+
+----------------
+Ideas of plugins
+----------------
+
+* DICOM-RT primitives (RT-STRUCT, RT-PLAN, RT-DOSE)
+* Converter to/from NIfTI
+* Decode JPEG2k with grok: https://github.com/GrokImageCompression/grok
+* Generate dynamic HTTP content using Lua:
+  https://groups.google.com/d/msg/orthanc-users/KompazkxRSs/5Rh03mzgDAAJ
+* More generally, expose more callbacks of the plugin SDK in Lua:
+  https://groups.google.com/d/msg/orthanc-users/_FbiRHuXPGM/J-OAv7zaCAAJ
+* Authorization plugin for the DICOM protocol:
+  https://groups.google.com/d/msg/orthanc-users/Wv-QEwTE0IA/rvJxoOjcAQAJ
+
+
+===
+Lua
+===
+
+* Configure HTTP headers from Lua (in HttpGet(), HttpPost(),
+  HttpPut(), HttpDelete(), RestApiGet(), RestApiPost(), RestApiPut()
+  and RestApiDelete().
+  https://groups.google.com/forum/#!msg/orthanc-users/WNnW187OILM/6XX_bm96BwAJ
+
+
+===========
+Performance
+===========
+
+
+================
+Code refactoring
+================
+
+* Use Semaphore::Locker everywhere (instead of explicit
+  Release() and Acquire())
+* Avoid direct calls to FromDcmtkBridge (make most of its 
+  methods private), go through ParsedDicomFile wherever possible
+
+
+=================
+Platform-specific
+=================
+
+---------
+Packaging
+---------
+
+* CentOS and RHEL through EPEL:
+  http://fedoraproject.org/wiki/EPEL_Package_Maintainers
+
+------------------------
+Big-endian architectures
+------------------------
+
+* Check the generated 16bpp PNG images
+
+-----------------
+Microsoft Windows
+-----------------
+
+* Add compatibility with non-ASCII paths (Orthanc expresses its paths
+  as UTF-8 strings, but Windows expects them to be translated to the 
+  system locale)
+
+
+=====================
+External applications
+=====================
+
+* Create REST bindings with Slicer
+* Create REST bindings with Horos/OsiriX
--- a/UnitTestsSources/DicomMapTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/DicomMapTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,10 +34,9 @@
 #include "PrecompiledHeadersUnitTests.h"
 #include "gtest/gtest.h"
 
-#include "../Core/Uuid.h"
 #include "../Core/OrthancException.h"
 #include "../Core/DicomFormat/DicomMap.h"
-#include "../OrthancServer/FromDcmtkBridge.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
 
 #include <memory>
 
@@ -90,7 +90,7 @@
 
   ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_NAME));
   ASSERT_FALSE(m.HasTag(0x0010, 0x0010));
-  m.SetValue(0x0010, 0x0010, "PatientName");
+  m.SetValue(0x0010, 0x0010, "PatientName", false);
   ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_NAME));
   ASSERT_TRUE(m.HasTag(0x0010, 0x0010));
 
@@ -99,9 +99,9 @@
   ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin());
 
   ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_ID));
-  m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID");
+  m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID", false);
   ASSERT_TRUE(m.HasTag(0x0010, 0x0020));
-  m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID2");
+  m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID2", false);
   ASSERT_EQ("PatientID2", m.GetValue(0x0010, 0x0020).GetContent());
 
   m.GetTags(s);
@@ -117,7 +117,7 @@
   std::auto_ptr<DicomMap> mm(m.Clone());
   ASSERT_EQ("PatientName", mm->GetValue(DICOM_TAG_PATIENT_NAME).GetContent());  
 
-  m.SetValue(DICOM_TAG_PATIENT_ID, "Hello");
+  m.SetValue(DICOM_TAG_PATIENT_ID, "Hello", false);
   ASSERT_THROW(mm->GetValue(DICOM_TAG_PATIENT_ID), OrthancException);
   mm->CopyTagIfExists(m, DICOM_TAG_PATIENT_ID);
   ASSERT_EQ("Hello", mm->GetValue(DICOM_TAG_PATIENT_ID).GetContent());  
@@ -151,6 +151,9 @@
 static void TestModule(ResourceType level,
                        DicomModule module)
 {
+  // REFERENCE: DICOM PS3.3 2015c - Information Object Definitions
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html
+
   std::set<DicomTag> moduleTags, main;
   DicomTag::AddTagsForModule(moduleTags, module);
   DicomMap::GetMainDicomTags(main, level);
@@ -160,28 +163,49 @@
   {
     bool ok = moduleTags.find(*it) != moduleTags.end();
 
-    // Exceptions for the Series level
-    /*if ((//
-          *it == DicomTag(0x, 0x) && 
-          level == ResourceType_Series))
+    // Exceptions for the Study level
+    if (level == ResourceType_Study &&
+        (*it == DicomTag(0x0008, 0x0080) ||  /* InstitutionName, from Visit identification module, related to Visit */
+         *it == DicomTag(0x0032, 0x1032) ||  /* RequestingPhysician, from Imaging Service Request module, related to Study */
+         *it == DicomTag(0x0032, 0x1060)))   /* RequestedProcedureDescription, from Requested Procedure module, related to Study */
     {
       ok = true;
-      }*/
+    }
+
+    // Exceptions for the Series level
+    if (level == ResourceType_Series &&
+        (*it == DicomTag(0x0008, 0x0070) ||  /* Manufacturer, from General Equipment Module */
+         *it == DicomTag(0x0008, 0x1010) ||  /* StationName, from General Equipment Module */
+         *it == DicomTag(0x0018, 0x0024) ||  /* SequenceName, from MR Image Module (SIMPLIFICATION => Series) */
+         *it == DicomTag(0x0018, 0x1090) ||  /* CardiacNumberOfImages, from MR Image Module (SIMPLIFICATION => Series) */
+         *it == DicomTag(0x0020, 0x0037) ||  /* ImageOrientationPatient, from Image Plane Module (SIMPLIFICATION => Series) */
+         *it == DicomTag(0x0020, 0x0105) ||  /* NumberOfTemporalPositions, from MR Image Module (SIMPLIFICATION => Series) */
+         *it == DicomTag(0x0020, 0x1002) ||  /* ImagesInAcquisition, from General Image Module (SIMPLIFICATION => Series) */
+         *it == DicomTag(0x0054, 0x0081) ||  /* NumberOfSlices, from PET Series module */
+         *it == DicomTag(0x0054, 0x0101) ||  /* NumberOfTimeSlices, from PET Series module */
+         *it == DicomTag(0x0054, 0x1000) ||  /* SeriesType, from PET Series module */
+         *it == DicomTag(0x0018, 0x1400) ||  /* AcquisitionDeviceProcessingDescription, from CR/X-Ray/DX/WholeSlideMicro Image (SIMPLIFICATION => Series) */
+         *it == DicomTag(0x0018, 0x0010)))   /* ContrastBolusAgent, from Contrast/Bolus module (SIMPLIFICATION => Series) */
+    {
+      ok = true;
+    }
 
     // Exceptions for the Instance level
     if (level == ResourceType_Instance &&
-        (*it == DicomTag(0x0020, 0x0012) ||  /* Accession number, from Image module */
-         *it == DicomTag(0x0054, 0x1330) ||  /* Image Index, from PET Image module */
-         *it == DicomTag(0x0020, 0x0100) ||  /* Temporal Position Identifier, from MR Image module */
-         *it == DicomTag(0x0028, 0x0008) ||  /* Number of Frames, from Multi-frame module attributes, related to Image IOD */
-         *it == DICOM_TAG_IMAGE_POSITION_PATIENT))
+        (*it == DicomTag(0x0020, 0x0012) ||  /* AccessionNumber, from General Image module */
+         *it == DicomTag(0x0054, 0x1330) ||  /* ImageIndex, from PET Image module */
+         *it == DicomTag(0x0020, 0x0100) ||  /* TemporalPositionIdentifier, from MR Image module */
+         *it == DicomTag(0x0028, 0x0008) ||  /* NumberOfFrames, from Multi-frame module attributes, related to Image */
+         *it == DicomTag(0x0020, 0x0032) ||  /* ImagePositionPatient, from Image Plan module, related to Image */
+         *it == DicomTag(0x0020, 0x0037) ||  /* ImageOrientationPatient, from Image Plane Module (Orthanc 1.4.2) */
+         *it == DicomTag(0x0020, 0x4000)))   /* ImageComments, from General Image module */
     {
       ok = true;
     }
 
     if (!ok)
     {
-      std::cout << it->Format() << ": " << FromDcmtkBridge::GetName(*it)
+      std::cout << it->Format() << ": " << FromDcmtkBridge::GetTagName(*it, "")
                 << " not expected at level " << EnumerationToString(level) << std::endl;
     }
 
@@ -194,6 +218,194 @@
 {
   TestModule(ResourceType_Patient, DicomModule_Patient);
   TestModule(ResourceType_Study, DicomModule_Study);
-  //TestModule(ResourceType_Series, DicomModule_Series);   // TODO
+  TestModule(ResourceType_Series, DicomModule_Series);   // TODO
   TestModule(ResourceType_Instance, DicomModule_Instance);
 }
+
+
+TEST(DicomMap, Parse)
+{
+  DicomMap m;
+  float f;
+  double d;
+  int32_t i;
+  int64_t j;
+  uint32_t k;
+  uint64_t l;
+  std::string s;
+  
+  m.SetValue(DICOM_TAG_PATIENT_NAME, "      ", false);  // Empty value
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l));
+  
+  m.SetValue(DICOM_TAG_PATIENT_NAME, "0", true);  // Binary value
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l));
+
+  ASSERT_FALSE(m.CopyToString(s, DICOM_TAG_PATIENT_NAME, false));
+  ASSERT_TRUE(m.CopyToString(s, DICOM_TAG_PATIENT_NAME, true));
+  ASSERT_EQ("0", s);
+               
+
+  // 2**31-1
+  m.SetValue(DICOM_TAG_PATIENT_NAME, "2147483647", false);
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l));
+  ASSERT_FLOAT_EQ(2147483647.0f, f);
+  ASSERT_DOUBLE_EQ(2147483647.0, d);
+  ASSERT_EQ(2147483647, i);
+  ASSERT_EQ(2147483647ll, j);
+  ASSERT_EQ(2147483647u, k);
+  ASSERT_EQ(2147483647ull, l);
+
+  // Test shortcuts
+  m.SetValue(DICOM_TAG_PATIENT_NAME, "42", false);
+  ASSERT_TRUE(m.ParseFloat(f, DICOM_TAG_PATIENT_NAME));
+  ASSERT_TRUE(m.ParseDouble(d, DICOM_TAG_PATIENT_NAME));
+  ASSERT_TRUE(m.ParseInteger32(i, DICOM_TAG_PATIENT_NAME));
+  ASSERT_TRUE(m.ParseInteger64(j, DICOM_TAG_PATIENT_NAME));
+  ASSERT_TRUE(m.ParseUnsignedInteger32(k, DICOM_TAG_PATIENT_NAME));
+  ASSERT_TRUE(m.ParseUnsignedInteger64(l, DICOM_TAG_PATIENT_NAME));
+  ASSERT_FLOAT_EQ(42.0f, f);
+  ASSERT_DOUBLE_EQ(42.0, d);
+  ASSERT_EQ(42, i);
+  ASSERT_EQ(42ll, j);
+  ASSERT_EQ(42u, k);
+  ASSERT_EQ(42ull, l);
+
+  ASSERT_TRUE(m.CopyToString(s, DICOM_TAG_PATIENT_NAME, false));
+  ASSERT_EQ("42", s);
+  ASSERT_TRUE(m.CopyToString(s, DICOM_TAG_PATIENT_NAME, true));
+  ASSERT_EQ("42", s);
+               
+  
+  // 2**31
+  m.SetValue(DICOM_TAG_PATIENT_NAME, "2147483648", false);
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l));
+  ASSERT_FLOAT_EQ(2147483648.0f, f);
+  ASSERT_DOUBLE_EQ(2147483648.0, d);
+  ASSERT_EQ(2147483648ll, j);
+  ASSERT_EQ(2147483648u, k);
+  ASSERT_EQ(2147483648ull, l);
+
+  // 2**32-1
+  m.SetValue(DICOM_TAG_PATIENT_NAME, "4294967295", false);
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l));
+  ASSERT_FLOAT_EQ(4294967295.0f, f);
+  ASSERT_DOUBLE_EQ(4294967295.0, d);
+  ASSERT_EQ(4294967295ll, j);
+  ASSERT_EQ(4294967295u, k);
+  ASSERT_EQ(4294967295ull, l);
+  
+  // 2**32
+  m.SetValue(DICOM_TAG_PATIENT_NAME, "4294967296", false);
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l));
+  ASSERT_FLOAT_EQ(4294967296.0f, f);
+  ASSERT_DOUBLE_EQ(4294967296.0, d);
+  ASSERT_EQ(4294967296ll, j);
+  ASSERT_EQ(4294967296ull, l);
+  
+  m.SetValue(DICOM_TAG_PATIENT_NAME, "-1", false);
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l));
+  ASSERT_FLOAT_EQ(-1.0f, f);
+  ASSERT_DOUBLE_EQ(-1.0, d);
+  ASSERT_EQ(-1, i);
+  ASSERT_EQ(-1ll, j);
+
+  // -2**31
+  m.SetValue(DICOM_TAG_PATIENT_NAME, "-2147483648", false);
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l));
+  ASSERT_FLOAT_EQ(-2147483648.0f, f);
+  ASSERT_DOUBLE_EQ(-2147483648.0, d);
+  ASSERT_EQ(static_cast<int32_t>(-2147483648ll), i);
+  ASSERT_EQ(-2147483648ll, j);
+  
+  // -2**31 - 1
+  m.SetValue(DICOM_TAG_PATIENT_NAME, "-2147483649", false);
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i));
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k));
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l));
+  ASSERT_FLOAT_EQ(-2147483649.0f, f);
+  ASSERT_DOUBLE_EQ(-2147483649.0, d); 
+  ASSERT_EQ(-2147483649ll, j);
+}
+
+
+TEST(DicomMap, Serialize)
+{
+  Json::Value s;
+  
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_PATIENT_NAME, "Hello", false);
+    m.SetValue(DICOM_TAG_STUDY_DESCRIPTION, "Binary", true);
+    m.SetNullValue(DICOM_TAG_SERIES_DESCRIPTION);
+    m.Serialize(s);
+  }
+
+  {
+    DicomMap m;
+    m.Unserialize(s);
+
+    const DicomValue* v = m.TestAndGetValue(DICOM_TAG_ACCESSION_NUMBER);
+    ASSERT_TRUE(v == NULL);
+
+    v = m.TestAndGetValue(DICOM_TAG_PATIENT_NAME);
+    ASSERT_TRUE(v != NULL);
+    ASSERT_FALSE(v->IsNull());
+    ASSERT_FALSE(v->IsBinary());
+    ASSERT_EQ("Hello", v->GetContent());
+
+    v = m.TestAndGetValue(DICOM_TAG_STUDY_DESCRIPTION);
+    ASSERT_TRUE(v != NULL);
+    ASSERT_FALSE(v->IsNull());
+    ASSERT_TRUE(v->IsBinary());
+    ASSERT_EQ("Binary", v->GetContent());
+
+    v = m.TestAndGetValue(DICOM_TAG_SERIES_DESCRIPTION);
+    ASSERT_TRUE(v != NULL);
+    ASSERT_TRUE(v->IsNull());
+    ASSERT_FALSE(v->IsBinary());
+    ASSERT_THROW(v->GetContent(), OrthancException);
+  }
+}
--- a/UnitTestsSources/FileStorageTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/FileStorageTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -42,7 +43,6 @@
 #include "../Core/Logging.h"
 #include "../Core/OrthancException.h"
 #include "../Core/Toolbox.h"
-#include "../Core/Uuid.h"
 #include "../OrthancServer/ServerIndex.h"
 
 using namespace Orthanc;
--- a/UnitTestsSources/FromDcmtkTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/FromDcmtkTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,24 +34,30 @@
 #include "PrecompiledHeadersUnitTests.h"
 #include "gtest/gtest.h"
 
-#include "../OrthancServer/FromDcmtkBridge.h"
-#include "../OrthancServer/OrthancInitialization.h"
-#include "../OrthancServer/DicomModification.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/DicomParsing/ToDcmtkBridge.h"
+#include "../Core/DicomParsing/DicomModification.h"
 #include "../OrthancServer/ServerToolbox.h"
 #include "../Core/OrthancException.h"
 #include "../Core/Images/ImageBuffer.h"
 #include "../Core/Images/PngReader.h"
 #include "../Core/Images/PngWriter.h"
-#include "../Core/Uuid.h"
+#include "../Core/Images/Image.h"
+#include "../Core/Images/ImageProcessing.h"
+#include "../Core/Endianness.h"
 #include "../Resources/EncodingTests.h"
+#include "../Core/DicomNetworking/DicomFindAnswers.h"
+#include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
+#include "../Plugins/Engine/PluginsEnumerations.h"
 
 #include <dcmtk/dcmdata/dcelem.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
 
 using namespace Orthanc;
 
 TEST(DicomFormat, Tag)
 {
-  ASSERT_EQ("PatientName", FromDcmtkBridge::GetName(DicomTag(0x0010, 0x0010)));
+  ASSERT_EQ("PatientName", FromDcmtkBridge::GetTagName(DicomTag(0x0010, 0x0010), ""));
 
   DicomTag t = FromDcmtkBridge::ParseTag("SeriesDescription");
   ASSERT_EQ(0x0008, t.GetGroup());
@@ -69,21 +76,21 @@
 TEST(DicomModification, Basic)
 {
   DicomModification m;
-  m.SetupAnonymization();
+  m.SetupAnonymization(DicomVersion_2008);
   //m.SetLevel(DicomRootLevel_Study);
-  //m.Replace(DICOM_TAG_PATIENT_ID, "coucou");
-  //m.Replace(DICOM_TAG_PATIENT_NAME, "coucou");
+  //m.ReplacePlainString(DICOM_TAG_PATIENT_ID, "coucou");
+  //m.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou");
 
-  ParsedDicomFile o;
+  ParsedDicomFile o(true);
   o.SaveToFile("UnitTestsResults/anon.dcm");
 
   for (int i = 0; i < 10; i++)
   {
     char b[1024];
     sprintf(b, "UnitTestsResults/anon%06d.dcm", i);
-    std::auto_ptr<ParsedDicomFile> f(o.Clone());
+    std::auto_ptr<ParsedDicomFile> f(o.Clone(false));
     if (i > 4)
-      o.Replace(DICOM_TAG_SERIES_INSTANCE_UID, "coucou");
+      o.ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, "coucou");
     m.Apply(*f);
     f->SaveToFile(b);
   }
@@ -96,28 +103,28 @@
 
   const DicomTag privateTag(0x0045, 0x0010);
   const DicomTag privateTag2(FromDcmtkBridge::ParseTag("0031-1020"));
-  ASSERT_TRUE(FromDcmtkBridge::IsPrivateTag(privateTag));
-  ASSERT_TRUE(FromDcmtkBridge::IsPrivateTag(privateTag2));
+  ASSERT_TRUE(privateTag.IsPrivate());
+  ASSERT_TRUE(privateTag2.IsPrivate());
   ASSERT_EQ(0x0031, privateTag2.GetGroup());
   ASSERT_EQ(0x1020, privateTag2.GetElement());
 
   std::string s;
-  ParsedDicomFile o;
-  o.Replace(DICOM_TAG_PATIENT_NAME, "coucou");
+  ParsedDicomFile o(true);
+  o.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou");
   ASSERT_FALSE(o.GetTagValue(s, privateTag));
   o.Insert(privateTag, "private tag", false);
   ASSERT_TRUE(o.GetTagValue(s, privateTag));
   ASSERT_STREQ("private tag", s.c_str());
 
   ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  ASSERT_THROW(o.Replace(privateTag2, "hello", DicomReplaceMode_ThrowIfAbsent), OrthancException);
+  ASSERT_THROW(o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_ThrowIfAbsent), OrthancException);
   ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  o.Replace(privateTag2, "hello", DicomReplaceMode_IgnoreIfAbsent);
+  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_IgnoreIfAbsent);
   ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  o.Replace(privateTag2, "hello", DicomReplaceMode_InsertIfAbsent);
+  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_InsertIfAbsent);
   ASSERT_TRUE(o.GetTagValue(s, privateTag2));
   ASSERT_STREQ("hello", s.c_str());
-  o.Replace(privateTag2, "hello world");
+  o.ReplacePlainString(privateTag2, "hello world");
   ASSERT_TRUE(o.GetTagValue(s, privateTag2));
   ASSERT_STREQ("hello world", s.c_str());
 
@@ -125,7 +132,7 @@
   ASSERT_FALSE(Toolbox::IsUuid(s));
 
   DicomModification m;
-  m.SetupAnonymization();
+  m.SetupAnonymization(DicomVersion_2008);
   m.Keep(privateTag);
 
   m.Apply(o);
@@ -135,7 +142,7 @@
   ASSERT_TRUE(o.GetTagValue(s, privateTag));
   ASSERT_STREQ("private tag", s.c_str());
   
-  m.SetupAnonymization();
+  m.SetupAnonymization(DicomVersion_2008);
   m.Apply(o);
   ASSERT_FALSE(o.GetTagValue(s, privateTag));
 }
@@ -149,7 +156,7 @@
   std::string s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
 
   std::string m, cc;
-  Toolbox::DecodeDataUriScheme(m, cc, s);
+  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(m, cc, s));
 
   ASSERT_EQ("image/png", m);
 
@@ -160,7 +167,7 @@
   ASSERT_EQ(5u, reader.GetWidth());
   ASSERT_EQ(PixelFormat_RGBA32, reader.GetFormat());
 
-  ParsedDicomFile o;
+  ParsedDicomFile o(true);
   o.EmbedContent(s);
   o.SaveToFile("UnitTestsResults/png1.dcm");
 
@@ -172,7 +179,7 @@
   // Check box in Graylevel8
   s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII=";
   o.EmbedContent(s);
-  //o.Replace(DICOM_TAG_SOP_CLASS_UID, UID_DigitalXRayImageStorageForProcessing);
+  //o.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, UID_DigitalXRayImageStorageForProcessing);
   o.SaveToFile("UnitTestsResults/png3.dcm");
 
 
@@ -184,17 +191,20 @@
     img.SetHeight(256);
     img.SetFormat(PixelFormat_Grayscale16);
 
+    ImageAccessor accessor;
+    img.GetWriteableAccessor(accessor);
+    
     uint16_t v = 0;
     for (unsigned int y = 0; y < img.GetHeight(); y++)
     {
-      uint16_t *p = reinterpret_cast<uint16_t*>(img.GetAccessor().GetRow(y));
+      uint16_t *p = reinterpret_cast<uint16_t*>(accessor.GetRow(y));
       for (unsigned int x = 0; x < img.GetWidth(); x++, p++, v++)
       {
         *p = v;
       }
     }
 
-    o.EmbedImage(img.GetAccessor());
+    o.EmbedImage(accessor);
     o.SaveToFile("UnitTestsResults/png4.dcm");
   }
 }
@@ -215,12 +225,13 @@
 
 TEST(FromDcmtkBridge, Enumerations)
 {
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
   Encoding e;
 
   ASSERT_FALSE(GetDicomEncoding(e, ""));
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 6"));  ASSERT_EQ(Encoding_Utf8, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 6"));  ASSERT_EQ(Encoding_Ascii, e);
 
-  // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ - Table C.12-2
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-2
   ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 100"));  ASSERT_EQ(Encoding_Latin1, e);
   ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 101"));  ASSERT_EQ(Encoding_Latin2, e);
   ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 109"));  ASSERT_EQ(Encoding_Latin3, e);
@@ -230,11 +241,11 @@
   ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 126"));  ASSERT_EQ(Encoding_Greek, e);
   ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 138"));  ASSERT_EQ(Encoding_Hebrew, e);
   ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 148"));  ASSERT_EQ(Encoding_Latin5, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 13"));  ASSERT_EQ(Encoding_Japanese, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 13"));   ASSERT_EQ(Encoding_Japanese, e);
   ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 166"));  ASSERT_EQ(Encoding_Thai, e);
 
-  // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ - Table C.12-3
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 6"));  ASSERT_EQ(Encoding_Utf8, e);
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-3
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 6"));    ASSERT_EQ(Encoding_Ascii, e);
   ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 100"));  ASSERT_EQ(Encoding_Latin1, e);
   ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 101"));  ASSERT_EQ(Encoding_Latin2, e);
   ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 109"));  ASSERT_EQ(Encoding_Latin3, e);
@@ -244,17 +255,18 @@
   ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 126"));  ASSERT_EQ(Encoding_Greek, e);
   ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 138"));  ASSERT_EQ(Encoding_Hebrew, e);
   ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 148"));  ASSERT_EQ(Encoding_Latin5, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 13"));  ASSERT_EQ(Encoding_Japanese, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 13"));   ASSERT_EQ(Encoding_Japanese, e);
   ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 166"));  ASSERT_EQ(Encoding_Thai, e);
 
-  // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ - Table C.12-4
-  ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 87"));  //ASSERT_EQ(Encoding_JapaneseKanji, e);
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-4
+  ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 87"));   //ASSERT_EQ(Encoding_JapaneseKanji, e);
   ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 159"));  //ASSERT_EQ(Encoding_JapaneseKanjiSupplementary, e);
   ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 149"));  //ASSERT_EQ(Encoding_Korean, e);
 
-  // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ - Table C.12-5
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-5
   ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 192"));  ASSERT_EQ(Encoding_Utf8, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "GB18030")); ASSERT_EQ(Encoding_Chinese, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "GB18030"));     ASSERT_EQ(Encoding_Chinese, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "GBK"));         ASSERT_EQ(Encoding_Chinese, e);
 }
 
 
@@ -266,7 +278,7 @@
     std::string dicom;
 
     {
-      ParsedDicomFile f;
+      ParsedDicomFile f(true);
       f.SetEncoding(testEncodings[i]);
 
       std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i]);
@@ -293,16 +305,59 @@
 
 TEST(FromDcmtkBridge, ValueRepresentation)
 {
-  ASSERT_EQ(ValueRepresentation_PatientName, 
-            FromDcmtkBridge::GetValueRepresentation(DICOM_TAG_PATIENT_NAME));
+  ASSERT_EQ(ValueRepresentation_PersonName, 
+            FromDcmtkBridge::LookupValueRepresentation(DICOM_TAG_PATIENT_NAME));
   ASSERT_EQ(ValueRepresentation_Date, 
-            FromDcmtkBridge::GetValueRepresentation(DicomTag(0x0008, 0x0020) /* StudyDate */));
+            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x0020) /* StudyDate */));
   ASSERT_EQ(ValueRepresentation_Time, 
-            FromDcmtkBridge::GetValueRepresentation(DicomTag(0x0008, 0x0030) /* StudyTime */));
+            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x0030) /* StudyTime */));
   ASSERT_EQ(ValueRepresentation_DateTime, 
-            FromDcmtkBridge::GetValueRepresentation(DicomTag(0x0008, 0x002a) /* AcquisitionDateTime */));
-  ASSERT_EQ(ValueRepresentation_Other, 
-            FromDcmtkBridge::GetValueRepresentation(DICOM_TAG_PATIENT_ID));
+            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x002a) /* AcquisitionDateTime */));
+  ASSERT_EQ(ValueRepresentation_NotSupported, 
+            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0001, 0x0001) /* some private tag */));
+}
+
+
+TEST(FromDcmtkBridge, ValueRepresentationConversions)
+{
+  ASSERT_EQ(1, ValueRepresentation_ApplicationEntity);
+  ASSERT_EQ(1, OrthancPluginValueRepresentation_AE);
+
+  for (int i = ValueRepresentation_ApplicationEntity;
+       i <= ValueRepresentation_NotSupported; i++)
+  {
+    ValueRepresentation vr = static_cast<ValueRepresentation>(i);
+
+    if (vr == ValueRepresentation_NotSupported)
+    {
+      ASSERT_THROW(ToDcmtkBridge::Convert(vr), OrthancException);
+      ASSERT_THROW(Plugins::Convert(vr), OrthancException);
+    }
+    else if (vr == ValueRepresentation_OtherDouble || 
+             vr == ValueRepresentation_OtherLong ||
+             vr == ValueRepresentation_UniversalResource ||
+             vr == ValueRepresentation_UnlimitedCharacters)
+    {
+      // These VR are not supported as of DCMTK 3.6.0
+      ASSERT_THROW(ToDcmtkBridge::Convert(vr), OrthancException);
+      ASSERT_EQ(OrthancPluginValueRepresentation_UN, Plugins::Convert(vr));
+    }
+    else
+    {
+      ASSERT_EQ(vr, FromDcmtkBridge::Convert(ToDcmtkBridge::Convert(vr)));
+
+      OrthancPluginValueRepresentation plugins = Plugins::Convert(vr);
+      ASSERT_EQ(vr, Plugins::Convert(plugins));
+    }
+  }
+
+  for (int i = OrthancPluginValueRepresentation_AE;
+       i <= OrthancPluginValueRepresentation_UT; i++)
+  {
+    OrthancPluginValueRepresentation plugins = static_cast<OrthancPluginValueRepresentation>(i);
+    ValueRepresentation orthanc = Plugins::Convert(plugins);
+    ASSERT_EQ(plugins, Plugins::Convert(orthanc));
+  }
 }
 
 
@@ -329,102 +384,135 @@
 }
 
 
-TEST(FromDcmtkBridge, FromJson)
+namespace Orthanc
 {
-  std::auto_ptr<DcmElement> element;
-
-  {
-    Json::Value a;
-    a = "Hello";
-    element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8));
-
-    Json::Value b;
-    FromDcmtkBridge::ToJson(b, *element, DicomToJsonFormat_Short, DicomToJsonFlags_Default, 0, Encoding_Ascii);
-    ASSERT_EQ("Hello", b["0010,0010"].asString());
-  }
-
+  // Namespace for the "FRIEND_TEST()" directive in "FromDcmtkBridge" to apply:
+  // https://github.com/google/googletest/blob/master/googletest/docs/AdvancedGuide.md#private-class-members
+  TEST(FromDcmtkBridge, FromJson)
   {
-    Json::Value a;
-    a = "Hello";
-    // Cannot assign a string to a sequence
-    ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, false, Encoding_Utf8)), OrthancException);
-  }
-
-  {
-    Json::Value a = Json::arrayValue;
-    a.append("Hello");
-    // Cannot assign an array to a string
-    ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8)), OrthancException);
-  }
-
-  {
-    Json::Value a;
-    a = "data:application/octet-stream;base64,SGVsbG8=";  // echo -n "Hello" | base64
-    element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, true, Encoding_Utf8));
-
-    Json::Value b;
-    FromDcmtkBridge::ToJson(b, *element, DicomToJsonFormat_Short, DicomToJsonFlags_Default, 0, Encoding_Ascii);
-    ASSERT_EQ("Hello", b["0010,0010"].asString());
-  }
-
-  {
-    Json::Value a = Json::arrayValue;
-    CreateSampleJson(a);
-    element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, true, Encoding_Utf8));
+    std::auto_ptr<DcmElement> element;
 
     {
+      Json::Value a;
+      a = "Hello";
+      element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8));
+
       Json::Value b;
-      FromDcmtkBridge::ToJson(b, *element, DicomToJsonFormat_Short, DicomToJsonFlags_Default, 0, Encoding_Ascii);
-      ASSERT_EQ(Json::arrayValue, b["0008,1110"].type());
-      ASSERT_EQ(2, b["0008,1110"].size());
-      
-      Json::Value::ArrayIndex i = (b["0008,1110"][0]["0010,0010"].asString() == "Hello") ? 0 : 1;
+      std::set<DicomTag> ignoreTagLength;
+      ignoreTagLength.insert(DICOM_TAG_PATIENT_ID);
+
+      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
+                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, ignoreTagLength);
+      ASSERT_TRUE(b.isMember("0010,0010"));
+      ASSERT_EQ("Hello", b["0010,0010"].asString());
+
+      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
+                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, ignoreTagLength);
+      ASSERT_TRUE(b["0010,0010"].isNull()); // "Hello" has more than 3 characters
 
-      ASSERT_EQ(3, b["0008,1110"][i].size());
-      ASSERT_EQ(2, b["0008,1110"][1 - i].size());
-      ASSERT_EQ(b["0008,1110"][i]["0010,0010"].asString(), "Hello");
-      ASSERT_EQ(b["0008,1110"][i]["0010,0020"].asString(), "World");
-      ASSERT_EQ(b["0008,1110"][i]["0008,1030"].asString(), "Toto");
-      ASSERT_EQ(b["0008,1110"][1 - i]["0010,0010"].asString(), "Hello2");
-      ASSERT_EQ(b["0008,1110"][1 - i]["0010,0020"].asString(), "World2");
+      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Full,
+                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, ignoreTagLength);
+      ASSERT_TRUE(b["0010,0010"].isObject());
+      ASSERT_EQ("PatientName", b["0010,0010"]["Name"].asString());
+      ASSERT_EQ("TooLong", b["0010,0010"]["Type"].asString());
+      ASSERT_TRUE(b["0010,0010"]["Value"].isNull());
+
+      ignoreTagLength.insert(DICOM_TAG_PATIENT_NAME);
+      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
+                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, ignoreTagLength);
+      ASSERT_EQ("Hello", b["0010,0010"].asString());
+    }
+
+    {
+      Json::Value a;
+      a = "Hello";
+      // Cannot assign a string to a sequence
+      ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, false, Encoding_Utf8)), OrthancException);
     }
 
     {
+      Json::Value a = Json::arrayValue;
+      a.append("Hello");
+      // Cannot assign an array to a string
+      ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8)), OrthancException);
+    }
+
+    {
+      Json::Value a;
+      a = "data:application/octet-stream;base64,SGVsbG8=";  // echo -n "Hello" | base64
+      element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, true, Encoding_Utf8));
+
       Json::Value b;
-      FromDcmtkBridge::ToJson(b, *element, DicomToJsonFormat_Full, DicomToJsonFlags_Default, 0, Encoding_Ascii);
+      std::set<DicomTag> ignoreTagLength;
+      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
+                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, ignoreTagLength);
+      ASSERT_EQ("Hello", b["0010,0010"].asString());
+    }
+
+    {
+      Json::Value a = Json::arrayValue;
+      CreateSampleJson(a);
+      element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, true, Encoding_Utf8));
 
-      Json::Value c;
-      Toolbox::SimplifyTags(c, b);
+      {
+        Json::Value b;
+        std::set<DicomTag> ignoreTagLength;
+        FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
+                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, ignoreTagLength);
+        ASSERT_EQ(Json::arrayValue, b["0008,1110"].type());
+        ASSERT_EQ(2u, b["0008,1110"].size());
+      
+        Json::Value::ArrayIndex i = (b["0008,1110"][0]["0010,0010"].asString() == "Hello") ? 0 : 1;
 
-      a[1]["PatientName"] = "Hello2";  // To remove the Data URI Scheme encoding
-      ASSERT_EQ(0, c["ReferencedStudySequence"].compare(a));
+        ASSERT_EQ(3u, b["0008,1110"][i].size());
+        ASSERT_EQ(2u, b["0008,1110"][1 - i].size());
+        ASSERT_EQ(b["0008,1110"][i]["0010,0010"].asString(), "Hello");
+        ASSERT_EQ(b["0008,1110"][i]["0010,0020"].asString(), "World");
+        ASSERT_EQ(b["0008,1110"][i]["0008,1030"].asString(), "Toto");
+        ASSERT_EQ(b["0008,1110"][1 - i]["0010,0010"].asString(), "Hello2");
+        ASSERT_EQ(b["0008,1110"][1 - i]["0010,0020"].asString(), "World2");
+      }
+
+      {
+        Json::Value b;
+        std::set<DicomTag> ignoreTagLength;
+        FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Full,
+                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, ignoreTagLength);
+
+        Json::Value c;
+        ServerToolbox::SimplifyTags(c, b, DicomToJsonFormat_Human);
+
+        a[1]["PatientName"] = "Hello2";  // To remove the Data URI Scheme encoding
+        ASSERT_EQ(0, c["ReferencedStudySequence"].compare(a));
+      }
     }
   }
 }
 
 
-
 TEST(ParsedDicomFile, InsertReplaceStrings)
 {
-  ParsedDicomFile f;
+  ParsedDicomFile f(true);
 
   f.Insert(DICOM_TAG_PATIENT_NAME, "World", false);
   ASSERT_THROW(f.Insert(DICOM_TAG_PATIENT_ID, "Hello", false), OrthancException);  // Already existing tag
-  f.Replace(DICOM_TAG_SOP_INSTANCE_UID, "Toto");  // (*)
-  f.Replace(DICOM_TAG_SOP_CLASS_UID, "Tata");  // (**)
+  f.ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, "Toto");  // (*)
+  f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "Tata");  // (**)
 
   std::string s;
+  ASSERT_FALSE(f.LookupTransferSyntax(s));
 
-  ASSERT_THROW(f.Replace(DICOM_TAG_ACCESSION_NUMBER, "Accession", DicomReplaceMode_ThrowIfAbsent), OrthancException);
-  f.Replace(DICOM_TAG_ACCESSION_NUMBER, "Accession", DicomReplaceMode_IgnoreIfAbsent);
+  ASSERT_THROW(f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"),
+                         false, DicomReplaceMode_ThrowIfAbsent), OrthancException);
+  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_IgnoreIfAbsent);
   ASSERT_FALSE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
-  f.Replace(DICOM_TAG_ACCESSION_NUMBER, "Accession", DicomReplaceMode_InsertIfAbsent);
+  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_InsertIfAbsent);
   ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
   ASSERT_EQ(s, "Accession");
-  f.Replace(DICOM_TAG_ACCESSION_NUMBER, "Accession2", DicomReplaceMode_IgnoreIfAbsent);
+  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession2"), false, DicomReplaceMode_IgnoreIfAbsent);
   ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
   ASSERT_EQ(s, "Accession2");
-  f.Replace(DICOM_TAG_ACCESSION_NUMBER, "Accession3", DicomReplaceMode_ThrowIfAbsent);
+  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession3"), false, DicomReplaceMode_ThrowIfAbsent);
   ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
   ASSERT_EQ(s, "Accession3");
 
@@ -445,7 +533,7 @@
 
 TEST(ParsedDicomFile, InsertReplaceJson)
 {
-  ParsedDicomFile f;
+  ParsedDicomFile f(true);
 
   Json::Value a;
   CreateSampleJson(a);
@@ -470,18 +558,18 @@
 
   {
     Json::Value b;
-    f.ToJson(b, DicomToJsonFormat_Full, DicomToJsonFlags_Default, 0);
+    f.DatasetToJson(b, DicomToJsonFormat_Full, DicomToJsonFlags_Default, 0);
 
     Json::Value c;
-    Toolbox::SimplifyTags(c, b);
+    ServerToolbox::SimplifyTags(c, b, DicomToJsonFormat_Human);
 
     ASSERT_EQ(0, c["ReferencedPatientSequence"].compare(a));
     ASSERT_NE(0, c["ReferencedStudySequence"].compare(a));  // Because Data URI Scheme decoding was enabled
   }
 
   a = "data:application/octet-stream;base64,VGF0YQ==";   // echo -n "Tata" | base64 
-  f.Replace(DICOM_TAG_SOP_INSTANCE_UID, a, false);  // (*)
-  f.Replace(DICOM_TAG_SOP_CLASS_UID, a, true);  // (**)
+  f.Replace(DICOM_TAG_SOP_INSTANCE_UID, a, false, DicomReplaceMode_InsertIfAbsent);  // (*)
+  f.Replace(DICOM_TAG_SOP_CLASS_UID, a, true, DicomReplaceMode_InsertIfAbsent);  // (**)
 
   std::string s;
   ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_INSTANCE_UID));
@@ -497,7 +585,7 @@
 
 TEST(ParsedDicomFile, JsonEncoding)
 {
-  ParsedDicomFile f;
+  ParsedDicomFile f(true);
 
   for (unsigned int i = 0; i < testEncodingsCount; i++)
   {
@@ -512,10 +600,10 @@
       }
 
       Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i]);
-      f.Replace(DICOM_TAG_PATIENT_NAME, s, false);
+      f.Replace(DICOM_TAG_PATIENT_NAME, s, false, DicomReplaceMode_InsertIfAbsent);
 
       Json::Value v;
-      f.ToJson(v, DicomToJsonFormat_Simple, DicomToJsonFlags_Default, 0);
+      f.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
       ASSERT_EQ(v["PatientName"].asString(), std::string(testEncodingsExpected[i]));
     }
   }
@@ -524,85 +612,705 @@
 
 TEST(ParsedDicomFile, ToJsonFlags1)
 {
-  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), EVR_PN, "MyPrivateTag", 1, 1);
-  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), EVR_PN, "Declared public tag", 1, 1);
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), ValueRepresentation_PersonName, "MyPrivateTag", 1, 1, "");
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), ValueRepresentation_PersonName, "Declared public tag", 1, 1, "");
 
-  ParsedDicomFile f;
+  ParsedDicomFile f(true);
   f.Insert(DicomTag(0x7050, 0x1000), "Some public tag", false);  // Even group => public tag
   f.Insert(DicomTag(0x7052, 0x1000), "Some unknown tag", false);  // Even group => public, unknown tag
   f.Insert(DicomTag(0x7053, 0x1000), "Some private tag", false);  // Odd group => private tag
 
   Json::Value v;
-  f.ToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
+  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
   ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(6, v.getMemberNames().size());
+  ASSERT_EQ(6u, v.getMemberNames().size());
   ASSERT_FALSE(v.isMember("7052,1000"));
   ASSERT_FALSE(v.isMember("7053,1000"));
   ASSERT_TRUE(v.isMember("7050,1000"));
   ASSERT_EQ(Json::stringValue, v["7050,1000"].type());
   ASSERT_EQ("Some public tag", v["7050,1000"].asString());
 
-  f.ToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePrivateTags, 0);
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_ConvertBinaryToNull), 0);
   ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(7, v.getMemberNames().size());
+  ASSERT_EQ(7u, v.getMemberNames().size());
+  ASSERT_FALSE(v.isMember("7052,1000"));
+  ASSERT_TRUE(v.isMember("7050,1000"));
+  ASSERT_TRUE(v.isMember("7053,1000"));
+  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
+  ASSERT_EQ(Json::nullValue, v["7053,1000"].type());
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePrivateTags, 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(7u, v.getMemberNames().size());
   ASSERT_FALSE(v.isMember("7052,1000"));
   ASSERT_TRUE(v.isMember("7050,1000"));
   ASSERT_TRUE(v.isMember("7053,1000"));
   ASSERT_EQ("Some public tag", v["7050,1000"].asString());
-  ASSERT_EQ(Json::nullValue, v["7053,1000"].type());  // TODO SHOULD BE STRING
+  std::string mime, content;
+  ASSERT_EQ(Json::stringValue, v["7053,1000"].type());
+  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7053,1000"].asString()));
+  ASSERT_EQ("application/octet-stream", mime);
+  ASSERT_EQ("Some private tag", content);
 
-  f.ToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludeUnknownTags, 0);
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_ConvertBinaryToNull), 0);
   ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(7, v.getMemberNames().size());
+  ASSERT_EQ(7u, v.getMemberNames().size());
   ASSERT_TRUE(v.isMember("7050,1000"));
   ASSERT_TRUE(v.isMember("7052,1000"));
   ASSERT_FALSE(v.isMember("7053,1000"));
   ASSERT_EQ("Some public tag", v["7050,1000"].asString());
-  ASSERT_EQ(Json::nullValue, v["7052,1000"].type());  // TODO SHOULD BE STRING
+  ASSERT_EQ(Json::nullValue, v["7052,1000"].type());
 
-  f.ToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludePrivateTags), 0);
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags), 0);
   ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(8, v.getMemberNames().size());
+  ASSERT_EQ(7u, v.getMemberNames().size());
+  ASSERT_TRUE(v.isMember("7050,1000"));
+  ASSERT_TRUE(v.isMember("7052,1000"));
+  ASSERT_FALSE(v.isMember("7053,1000"));
+  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
+  ASSERT_EQ(Json::stringValue, v["7052,1000"].type());
+  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7052,1000"].asString()));
+  ASSERT_EQ("application/octet-stream", mime);
+  ASSERT_EQ("Some unknown tag", content);
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_ConvertBinaryToNull), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(8u, v.getMemberNames().size());
   ASSERT_TRUE(v.isMember("7050,1000"));
   ASSERT_TRUE(v.isMember("7052,1000"));
   ASSERT_TRUE(v.isMember("7053,1000"));
   ASSERT_EQ("Some public tag", v["7050,1000"].asString());
-  ASSERT_EQ(Json::nullValue, v["7052,1000"].type());  // TODO SHOULD BE STRING
-  ASSERT_EQ(Json::nullValue, v["7053,1000"].type());  // TODO SHOULD BE STRING
+  ASSERT_EQ(Json::nullValue, v["7052,1000"].type());
+  ASSERT_EQ(Json::nullValue, v["7053,1000"].type());
 }
 
 
 TEST(ParsedDicomFile, ToJsonFlags2)
 {
-  ParsedDicomFile f;
+  ParsedDicomFile f(true);
   f.Insert(DICOM_TAG_PIXEL_DATA, "Pixels", false);
 
   Json::Value v;
-  f.ToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
+  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
   ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(5, v.getMemberNames().size());
+  ASSERT_EQ(5u, v.getMemberNames().size());
   ASSERT_FALSE(v.isMember("7fe0,0010"));  
 
-  f.ToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToNull), 0);
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToNull), 0);
   ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(6, v.getMemberNames().size());
+  ASSERT_EQ(6u, v.getMemberNames().size());
   ASSERT_TRUE(v.isMember("7fe0,0010"));  
   ASSERT_EQ(Json::nullValue, v["7fe0,0010"].type());  
 
-  f.ToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToAscii), 0);
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToAscii), 0);
   ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(6, v.getMemberNames().size());
+  ASSERT_EQ(6u, v.getMemberNames().size());
   ASSERT_TRUE(v.isMember("7fe0,0010"));  
   ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type());  
   ASSERT_EQ("Pixels", v["7fe0,0010"].asString());  
 
-  f.ToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePixelData, 0);
+  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePixelData, 0);
   ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(6, v.getMemberNames().size());
+  ASSERT_EQ(6u, v.getMemberNames().size());
   ASSERT_TRUE(v.isMember("7fe0,0010"));  
   ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type());
   std::string mime, content;
-  Toolbox::DecodeDataUriScheme(mime, content, v["7fe0,0010"].asString());
+  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7fe0,0010"].asString()));
   ASSERT_EQ("application/octet-stream", mime);
   ASSERT_EQ("Pixels", content);
 }
+
+
+TEST(DicomFindAnswers, Basic)
+{
+  DicomFindAnswers a(false);
+
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_PATIENT_ID, "hello", false);
+    a.Add(m);
+  }
+
+  {
+    ParsedDicomFile d(true);
+    d.ReplacePlainString(DICOM_TAG_PATIENT_ID, "my");
+    a.Add(d);
+  }
+
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_PATIENT_ID, "world", false);
+    a.Add(m);
+  }
+
+  Json::Value j;
+  a.ToJson(j, true);
+  ASSERT_EQ(3u, j.size());
+
+  //std::cout << j;
+}
+
+
+TEST(ParsedDicomFile, FromJson)
+{
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7057, 0x1000), ValueRepresentation_OtherByte, "MyPrivateTag2", 1, 1, "ORTHANC");
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7059, 0x1000), ValueRepresentation_OtherByte, "MyPrivateTag3", 1, 1, "");
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), ValueRepresentation_PersonName, "Declared public tag2", 1, 1, "");
+
+  Json::Value v;
+  const std::string sopClassUid = "1.2.840.10008.5.1.4.1.1.1";  // CR Image Storage:
+
+  // Test the private creator
+  ASSERT_EQ(DcmTag_ERROR_TagName, FromDcmtkBridge::GetTagName(DicomTag(0x7057, 0x1000), "NOPE"));
+  ASSERT_EQ("MyPrivateTag2", FromDcmtkBridge::GetTagName(DicomTag(0x7057, 0x1000), "ORTHANC"));
+
+  {
+    v["SOPClassUID"] = sopClassUid;
+    v["SpecificCharacterSet"] = "ISO_IR 148";    // This is latin-5
+    v["PatientName"] = "Sébastien";
+    v["7050-1000"] = "Some public tag";  // Even group => public tag
+    v["7052-1000"] = "Some unknown tag";  // Even group => public, unknown tag
+    v["7057-1000"] = "Some private tag";  // Odd group => private tag
+    v["7059-1000"] = "Some private tag2";  // Odd group => private tag, with an odd length to test padding
+  
+    std::string s;
+    Toolbox::EncodeDataUriScheme(s, "application/octet-stream", "Sebastien");
+    v["StudyDescription"] = s;
+
+    v["PixelData"] = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";  // A red dot of 5x5 pixels
+    v["0040,0100"] = Json::arrayValue;  // ScheduledProcedureStepSequence
+
+    Json::Value vv;
+    vv["Modality"] = "MR";
+    v["0040,0100"].append(vv);
+
+    vv["Modality"] = "CT";
+    v["0040,0100"].append(vv);
+  }
+
+  const DicomToJsonFlags toJsonFlags = static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeBinary |
+                                                                     DicomToJsonFlags_IncludePixelData | 
+                                                                     DicomToJsonFlags_IncludePrivateTags | 
+                                                                     DicomToJsonFlags_IncludeUnknownTags | 
+                                                                     DicomToJsonFlags_ConvertBinaryToAscii);
+
+
+  {
+    std::auto_ptr<ParsedDicomFile> dicom
+      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_GenerateIdentifiers)));
+
+    Json::Value vv;
+    dicom->DatasetToJson(vv, DicomToJsonFormat_Human, toJsonFlags, 0);
+
+    ASSERT_EQ(vv["SOPClassUID"].asString(), sopClassUid);
+    ASSERT_EQ(vv["MediaStorageSOPClassUID"].asString(), sopClassUid);
+    ASSERT_TRUE(vv.isMember("SOPInstanceUID"));
+    ASSERT_TRUE(vv.isMember("SeriesInstanceUID"));
+    ASSERT_TRUE(vv.isMember("StudyInstanceUID"));
+    ASSERT_TRUE(vv.isMember("PatientID"));
+  }
+
+
+  {
+    std::auto_ptr<ParsedDicomFile> dicom
+      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_GenerateIdentifiers)));
+
+    Json::Value vv;
+    dicom->DatasetToJson(vv, DicomToJsonFormat_Human, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData), 0);
+
+    std::string mime, content;
+    ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, vv["PixelData"].asString()));
+    ASSERT_EQ("application/octet-stream", mime);
+    ASSERT_EQ(5u * 5u * 3u /* the red dot is 5x5 pixels in RGB24 */ + 1 /* for padding */, content.size());
+  }
+
+
+  {
+    std::auto_ptr<ParsedDicomFile> dicom
+      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_DecodeDataUriScheme)));
+
+    Json::Value vv;
+    dicom->DatasetToJson(vv, DicomToJsonFormat_Short, toJsonFlags, 0);
+
+    ASSERT_FALSE(vv.isMember("SOPInstanceUID"));
+    ASSERT_FALSE(vv.isMember("SeriesInstanceUID"));
+    ASSERT_FALSE(vv.isMember("StudyInstanceUID"));
+    ASSERT_FALSE(vv.isMember("PatientID"));
+    ASSERT_EQ(2u, vv["0040,0100"].size());
+    ASSERT_EQ("MR", vv["0040,0100"][0]["0008,0060"].asString());
+    ASSERT_EQ("CT", vv["0040,0100"][1]["0008,0060"].asString());
+    ASSERT_EQ("Some public tag", vv["7050,1000"].asString());
+    ASSERT_EQ("Some unknown tag", vv["7052,1000"].asString());
+    ASSERT_EQ("Some private tag", vv["7057,1000"].asString());
+    ASSERT_EQ("Some private tag2", vv["7059,1000"].asString());
+    ASSERT_EQ("Sébastien", vv["0010,0010"].asString());
+    ASSERT_EQ("Sebastien", vv["0008,1030"].asString());
+    ASSERT_EQ("ISO_IR 148", vv["0008,0005"].asString());
+    ASSERT_EQ("5", vv[DICOM_TAG_ROWS.Format()].asString());
+    ASSERT_EQ("5", vv[DICOM_TAG_COLUMNS.Format()].asString());
+    ASSERT_TRUE(vv[DICOM_TAG_PIXEL_DATA.Format()].asString().empty());
+  }
+}
+
+
+
+TEST(TestImages, PatternGrayscale8)
+{
+  static const char* PATH = "UnitTestsResults/PatternGrayscale8.dcm";
+
+  Orthanc::Image image(Orthanc::PixelFormat_Grayscale8, 256, 256, false);
+
+  for (int y = 0; y < 256; y++)
+  {
+    uint8_t *p = reinterpret_cast<uint8_t*>(image.GetRow(y));
+    for (int x = 0; x < 256; x++, p++)
+    {
+      *p = y;
+    }
+  }
+
+  Orthanc::ImageAccessor r;
+
+  image.GetRegion(r, 32, 32, 64, 192);
+  Orthanc::ImageProcessing::Set(r, 0);
+  
+  image.GetRegion(r, 160, 32, 64, 192);
+  Orthanc::ImageProcessing::Set(r, 255); 
+
+  {
+    ParsedDicomFile f(true);
+    f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7");
+    f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc");
+    f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns");
+    f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "Grayscale8");
+    f.EmbedImage(image);
+
+    f.SaveToFile(PATH);
+  }
+
+  {
+    std::string s;
+    Orthanc::SystemToolbox::ReadFile(s, PATH);
+    Orthanc::ParsedDicomFile f(s);
+    
+    std::auto_ptr<Orthanc::ImageAccessor> decoded(Orthanc::DicomImageDecoder::Decode(f, 0));
+    ASSERT_EQ(256u, decoded->GetWidth());
+    ASSERT_EQ(256u, decoded->GetHeight());
+    ASSERT_EQ(Orthanc::PixelFormat_Grayscale8, decoded->GetFormat());
+
+    for (int y = 0; y < 256; y++)
+    {
+      const void* a = image.GetConstRow(y);
+      const void* b = decoded->GetConstRow(y);
+      ASSERT_EQ(0, memcmp(a, b, 256));
+    }
+  }
+}
+
+
+TEST(TestImages, PatternRGB)
+{
+  static const char* PATH = "UnitTestsResults/PatternRGB24.dcm";
+
+  Orthanc::Image image(Orthanc::PixelFormat_RGB24, 384, 256, false);
+
+  for (int y = 0; y < 256; y++)
+  {
+    uint8_t *p = reinterpret_cast<uint8_t*>(image.GetRow(y));
+    for (int x = 0; x < 128; x++, p += 3)
+    {
+      p[0] = y;
+      p[1] = 0;
+      p[2] = 0;
+    }
+    for (int x = 128; x < 128 * 2; x++, p += 3)
+    {
+      p[0] = 0;
+      p[1] = 255 - y;
+      p[2] = 0;
+    }
+    for (int x = 128 * 2; x < 128 * 3; x++, p += 3)
+    {
+      p[0] = 0;
+      p[1] = 0;
+      p[2] = y;
+    }
+  }
+
+  {
+    ParsedDicomFile f(true);
+    f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7");
+    f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc");
+    f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns");
+    f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "RGB24");
+    f.EmbedImage(image);
+
+    f.SaveToFile(PATH);
+  }
+
+  {
+    std::string s;
+    Orthanc::SystemToolbox::ReadFile(s, PATH);
+    Orthanc::ParsedDicomFile f(s);
+    
+    std::auto_ptr<Orthanc::ImageAccessor> decoded(Orthanc::DicomImageDecoder::Decode(f, 0));
+    ASSERT_EQ(384u, decoded->GetWidth());
+    ASSERT_EQ(256u, decoded->GetHeight());
+    ASSERT_EQ(Orthanc::PixelFormat_RGB24, decoded->GetFormat());
+
+    for (int y = 0; y < 256; y++)
+    {
+      const void* a = image.GetConstRow(y);
+      const void* b = decoded->GetConstRow(y);
+      ASSERT_EQ(0, memcmp(a, b, 3 * 384));
+    }
+  }
+}
+
+
+TEST(TestImages, PatternUint16)
+{
+  static const char* PATH = "UnitTestsResults/PatternGrayscale16.dcm";
+
+  Orthanc::Image image(Orthanc::PixelFormat_Grayscale16, 256, 256, false);
+
+  uint16_t v = 0;
+  for (int y = 0; y < 256; y++)
+  {
+    uint16_t *p = reinterpret_cast<uint16_t*>(image.GetRow(y));
+    for (int x = 0; x < 256; x++, v++, p++)
+    {
+      *p = htole16(v);   // Orthanc uses Little-Endian transfer syntax to encode images
+    }
+  }
+
+  Orthanc::ImageAccessor r;
+  
+  image.GetRegion(r, 32, 32, 64, 192);
+  Orthanc::ImageProcessing::Set(r, 0);
+  
+  image.GetRegion(r, 160, 32, 64, 192);
+  Orthanc::ImageProcessing::Set(r, 65535); 
+
+  {
+    ParsedDicomFile f(true);
+    f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7");
+    f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc");
+    f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns");
+    f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "Grayscale16");
+    f.EmbedImage(image);
+
+    f.SaveToFile(PATH);
+  }
+
+  {
+    std::string s;
+    Orthanc::SystemToolbox::ReadFile(s, PATH);
+    Orthanc::ParsedDicomFile f(s);
+    
+    std::auto_ptr<Orthanc::ImageAccessor> decoded(Orthanc::DicomImageDecoder::Decode(f, 0));
+    ASSERT_EQ(256u, decoded->GetWidth());
+    ASSERT_EQ(256u, decoded->GetHeight());
+    ASSERT_EQ(Orthanc::PixelFormat_Grayscale16, decoded->GetFormat());
+
+    for (int y = 0; y < 256; y++)
+    {
+      const void* a = image.GetConstRow(y);
+      const void* b = decoded->GetConstRow(y);
+      ASSERT_EQ(0, memcmp(a, b, 512));
+    }
+  }
+}
+
+
+TEST(TestImages, PatternInt16)
+{
+  static const char* PATH = "UnitTestsResults/PatternSignedGrayscale16.dcm";
+
+  Orthanc::Image image(Orthanc::PixelFormat_SignedGrayscale16, 256, 256, false);
+
+  int16_t v = -32768;
+  for (int y = 0; y < 256; y++)
+  {
+    int16_t *p = reinterpret_cast<int16_t*>(image.GetRow(y));
+    for (int x = 0; x < 256; x++, v++, p++)
+    {
+      *p = htole16(v);   // Orthanc uses Little-Endian transfer syntax to encode images
+    }
+  }
+
+  Orthanc::ImageAccessor r;
+  image.GetRegion(r, 32, 32, 64, 192);
+  Orthanc::ImageProcessing::Set(r, -32768);
+  
+  image.GetRegion(r, 160, 32, 64, 192);
+  Orthanc::ImageProcessing::Set(r, 32767); 
+
+  {
+    ParsedDicomFile f(true);
+    f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7");
+    f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc");
+    f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns");
+    f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "SignedGrayscale16");
+    f.EmbedImage(image);
+
+    f.SaveToFile(PATH);
+  }
+
+  {
+    std::string s;
+    Orthanc::SystemToolbox::ReadFile(s, PATH);
+    Orthanc::ParsedDicomFile f(s);
+    
+    std::auto_ptr<Orthanc::ImageAccessor> decoded(Orthanc::DicomImageDecoder::Decode(f, 0));
+    ASSERT_EQ(256u, decoded->GetWidth());
+    ASSERT_EQ(256u, decoded->GetHeight());
+    ASSERT_EQ(Orthanc::PixelFormat_SignedGrayscale16, decoded->GetFormat());
+
+    for (int y = 0; y < 256; y++)
+    {
+      const void* a = image.GetConstRow(y);
+      const void* b = decoded->GetConstRow(y);
+      ASSERT_EQ(0, memcmp(a, b, 512));
+    }
+  }
+}
+
+
+
+static void CheckEncoding(const ParsedDicomFile& dicom,
+                          Encoding expected)
+{
+  const char* value = NULL;
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->findAndGetString(DCM_SpecificCharacterSet, value).good());
+
+  Encoding encoding;
+  ASSERT_TRUE(GetDicomEncoding(encoding, value));
+  ASSERT_EQ(expected, encoding);
+}
+
+
+TEST(ParsedDicomFile, DicomMapEncodings1)
+{
+  SetDefaultDicomEncoding(Encoding_Ascii);
+  ASSERT_EQ(Encoding_Ascii, GetDefaultDicomEncoding());
+
+  {
+    DicomMap m;
+    ParsedDicomFile dicom(m);
+    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
+    CheckEncoding(dicom, Encoding_Ascii);
+  }
+
+  {
+    DicomMap m;
+    ParsedDicomFile dicom(m, Encoding_Latin4);
+    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
+    CheckEncoding(dicom, Encoding_Latin4);
+  }
+
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 148", false);
+    ParsedDicomFile dicom(m);
+    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
+    CheckEncoding(dicom, Encoding_Latin5);
+  }
+
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 148", false);
+    ParsedDicomFile dicom(m, Encoding_Latin1);
+    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
+    CheckEncoding(dicom, Encoding_Latin5);
+  }
+}
+
+
+TEST(ParsedDicomFile, DicomMapEncodings2)
+{
+  const char* utf8 = NULL;
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    if (testEncodings[i] == Encoding_Utf8)
+    {
+      utf8 = testEncodingsEncoded[i];
+      break;
+    }
+  }  
+
+  ASSERT_TRUE(utf8 != NULL);
+
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    // 1251 codepage is not supported by the core DICOM standard, ignore it
+    if (testEncodings[i] != Encoding_Windows1251) 
+    {
+      {
+        // Sanity check to test the proper behavior of "EncodingTests.py"
+        std::string encoded = Toolbox::ConvertFromUtf8(testEncodingsExpected[i], testEncodings[i]);
+        ASSERT_STREQ(testEncodingsEncoded[i], encoded.c_str());
+        std::string decoded = Toolbox::ConvertToUtf8(encoded, testEncodings[i]);
+        ASSERT_STREQ(testEncodingsExpected[i], decoded.c_str());
+
+        if (testEncodings[i] != Encoding_Chinese)
+        {
+          // A specific source string is used in "EncodingTests.py" to
+          // test against Chinese, it is normal that it does not correspond to UTF8
+
+          std::string encoded = Toolbox::ConvertToUtf8(Toolbox::ConvertFromUtf8(utf8, testEncodings[i]), testEncodings[i]);
+          ASSERT_STREQ(testEncodingsExpected[i], encoded.c_str());
+        }
+      }
+
+
+      Json::Value v;
+
+      {
+        DicomMap m;
+        m.SetValue(DICOM_TAG_PATIENT_NAME, testEncodingsExpected[i], false);
+
+        ParsedDicomFile dicom(m, testEncodings[i]);
+    
+        const char* encoded = NULL;
+        ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->findAndGetString(DCM_PatientName, encoded).good());
+        ASSERT_STREQ(testEncodingsEncoded[i], encoded);
+
+        dicom.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
+
+        Encoding encoding;
+        ASSERT_TRUE(GetDicomEncoding(encoding, v["SpecificCharacterSet"].asCString()));
+        ASSERT_EQ(encoding, testEncodings[i]);
+        ASSERT_STREQ(testEncodingsExpected[i], v["PatientName"].asCString());
+      }
+
+
+      {
+        DicomMap m;
+        m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, GetDicomSpecificCharacterSet(testEncodings[i]), false);
+        m.SetValue(DICOM_TAG_PATIENT_NAME, testEncodingsExpected[i], false);
+
+        ParsedDicomFile dicom(m, testEncodings[i]);
+
+        Json::Value v2;
+        dicom.DatasetToJson(v2, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
+        
+        ASSERT_EQ(v2["PatientName"].asString(), v["PatientName"].asString());
+        ASSERT_EQ(v2["SpecificCharacterSet"].asString(), v["SpecificCharacterSet"].asString());
+      }
+    }
+  }
+}
+
+
+TEST(ParsedDicomFile, ChangeEncoding)
+{
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    // 1251 codepage is not supported by the core DICOM standard, ignore it
+    if (testEncodings[i] != Encoding_Windows1251) 
+    {
+      DicomMap m;
+      m.SetValue(DICOM_TAG_PATIENT_NAME, testEncodingsExpected[i], false);
+
+      std::string tag;
+
+      ParsedDicomFile dicom(m, Encoding_Utf8);
+      ASSERT_EQ(Encoding_Utf8, dicom.GetEncoding());
+      ASSERT_TRUE(dicom.GetTagValue(tag, DICOM_TAG_PATIENT_NAME));
+      ASSERT_EQ(tag, testEncodingsExpected[i]);
+
+      {
+        Json::Value v;
+        dicom.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
+        ASSERT_STREQ(v["SpecificCharacterSet"].asCString(), "ISO_IR 192");
+        ASSERT_STREQ(v["PatientName"].asCString(), testEncodingsExpected[i]);
+      }
+
+      dicom.ChangeEncoding(testEncodings[i]);
+
+      ASSERT_EQ(testEncodings[i], dicom.GetEncoding());
+      
+      const char* c = NULL;
+      ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->findAndGetString(DCM_PatientName, c).good());
+      EXPECT_STREQ(c, testEncodingsEncoded[i]);
+      
+      ASSERT_TRUE(dicom.GetTagValue(tag, DICOM_TAG_PATIENT_NAME));  // Decodes to UTF-8
+      EXPECT_EQ(tag, testEncodingsExpected[i]);
+
+      {
+        Json::Value v;
+        dicom.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
+        ASSERT_STREQ(v["SpecificCharacterSet"].asCString(), GetDicomSpecificCharacterSet(testEncodings[i]));
+        ASSERT_STREQ(v["PatientName"].asCString(), testEncodingsExpected[i]);
+      }
+    }
+  }
+}
+
+
+TEST(Toolbox, CaseWithAccents)
+{
+  ASSERT_EQ(toUpperResult, Toolbox::ToUpperCaseWithAccents(toUpperSource));
+}
+
+
+
+TEST(ParsedDicomFile, InvalidCharacterSets)
+{
+  {
+    // No encoding provided, fallback to default encoding
+    DicomMap m;
+    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
+
+    ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */);
+    ASSERT_EQ(Encoding_Latin3, d.GetEncoding());
+  }
+  
+  {
+    // Valid encoding, "ISO_IR 13" is Japanese
+    DicomMap m;
+    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 13", false);
+    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
+
+    ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */);
+    ASSERT_EQ(Encoding_Japanese, d.GetEncoding());
+  }
+  
+  {
+    // Invalid value for an encoding ("nope" is not in the DICOM standard)
+    DicomMap m;
+    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "nope", false);
+    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
+
+    ASSERT_THROW(ParsedDicomFile d(m, Encoding_Latin3), OrthancException);
+  }
+  
+  {
+    // Invalid encoding, as provided as a binary string
+    DicomMap m;
+    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 13", true);
+    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
+
+    ASSERT_THROW(ParsedDicomFile d(m, Encoding_Latin3), OrthancException);
+  }
+  
+  {
+    // Encoding provided as an empty string, fallback to default encoding
+    // In Orthanc <= 1.3.1, this test was throwing an exception
+    DicomMap m;
+    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "", false);
+    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
+
+    ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */);
+    ASSERT_EQ(Encoding_Latin3, d.GetEncoding());
+  }
+}
--- a/UnitTestsSources/ImageProcessingTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/ImageProcessingTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,8 +35,11 @@
 #include "gtest/gtest.h"
 
 #include "../Core/DicomFormat/DicomImageInformation.h"
-#include "../Core/Images/ImageBuffer.h"
+#include "../Core/Images/Image.h"
 #include "../Core/Images/ImageProcessing.h"
+#include "../Core/Images/ImageTraits.h"
+
+#include <memory>
 
 using namespace Orthanc;
 
@@ -44,18 +48,18 @@
 {
   // Cardiac/MR*
   DicomMap m;
-  m.SetValue(DICOM_TAG_ROWS, "24");
-  m.SetValue(DICOM_TAG_COLUMNS, "16");
-  m.SetValue(DICOM_TAG_BITS_ALLOCATED, "16");
-  m.SetValue(DICOM_TAG_SAMPLES_PER_PIXEL, "1");
-  m.SetValue(DICOM_TAG_BITS_STORED, "12");
-  m.SetValue(DICOM_TAG_HIGH_BIT, "11");
-  m.SetValue(DICOM_TAG_PIXEL_REPRESENTATION, "0");
-  m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
+  m.SetValue(DICOM_TAG_ROWS, "24", false);
+  m.SetValue(DICOM_TAG_COLUMNS, "16", false);
+  m.SetValue(DICOM_TAG_BITS_ALLOCATED, "16", false);
+  m.SetValue(DICOM_TAG_SAMPLES_PER_PIXEL, "1", false);
+  m.SetValue(DICOM_TAG_BITS_STORED, "12", false);
+  m.SetValue(DICOM_TAG_HIGH_BIT, "11", false);
+  m.SetValue(DICOM_TAG_PIXEL_REPRESENTATION, "0", false);
+  m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2", false);
 
   DicomImageInformation info(m);
   PixelFormat format;
-  ASSERT_TRUE(info.ExtractPixelFormat(format));
+  ASSERT_TRUE(info.ExtractPixelFormat(format, false));
   ASSERT_EQ(PixelFormat_Grayscale16, format);
 }
 
@@ -64,17 +68,133 @@
 {
   // Delphine CT
   DicomMap m;
-  m.SetValue(DICOM_TAG_ROWS, "24");
-  m.SetValue(DICOM_TAG_COLUMNS, "16");
-  m.SetValue(DICOM_TAG_BITS_ALLOCATED, "16");
-  m.SetValue(DICOM_TAG_SAMPLES_PER_PIXEL, "1");
-  m.SetValue(DICOM_TAG_BITS_STORED, "16");
-  m.SetValue(DICOM_TAG_HIGH_BIT, "15");
-  m.SetValue(DICOM_TAG_PIXEL_REPRESENTATION, "1");
-  m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
+  m.SetValue(DICOM_TAG_ROWS, "24", false);
+  m.SetValue(DICOM_TAG_COLUMNS, "16", false);
+  m.SetValue(DICOM_TAG_BITS_ALLOCATED, "16", false);
+  m.SetValue(DICOM_TAG_SAMPLES_PER_PIXEL, "1", false);
+  m.SetValue(DICOM_TAG_BITS_STORED, "16", false);
+  m.SetValue(DICOM_TAG_HIGH_BIT, "15", false);
+  m.SetValue(DICOM_TAG_PIXEL_REPRESENTATION, "1", false);
+  m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2", false);
 
   DicomImageInformation info(m);
   PixelFormat format;
-  ASSERT_TRUE(info.ExtractPixelFormat(format));
+  ASSERT_TRUE(info.ExtractPixelFormat(format, false));
   ASSERT_EQ(PixelFormat_SignedGrayscale16, format);
 }
+
+
+
+namespace
+{
+  template <typename T>
+  class TestImageTraits : public ::testing::Test
+  {
+  private:
+    std::auto_ptr<Image>  image_;
+
+  protected:
+    virtual void SetUp() 
+    {
+      image_.reset(new Image(ImageTraits::PixelTraits::GetPixelFormat(), 7, 9, false));
+    }
+
+    virtual void TearDown()
+    {
+      image_.reset(NULL);
+    }
+
+  public:
+    typedef T ImageTraits;
+    
+    ImageAccessor& GetImage()
+    {
+      return *image_;
+    }
+  };
+
+  template <typename T>
+  class TestIntegerImageTraits : public TestImageTraits<T>
+  {
+  };
+}
+
+
+typedef ::testing::Types<
+  ImageTraits<PixelFormat_Grayscale8>,
+  ImageTraits<PixelFormat_Grayscale16>,
+  ImageTraits<PixelFormat_SignedGrayscale16>
+  > IntegerFormats;
+TYPED_TEST_CASE(TestIntegerImageTraits, IntegerFormats);
+
+typedef ::testing::Types<
+  ImageTraits<PixelFormat_Grayscale8>,
+  ImageTraits<PixelFormat_Grayscale16>,
+  ImageTraits<PixelFormat_SignedGrayscale16>,
+  ImageTraits<PixelFormat_RGB24>,
+  ImageTraits<PixelFormat_BGRA32>
+  > AllFormats;
+TYPED_TEST_CASE(TestImageTraits, AllFormats);
+
+
+TYPED_TEST(TestImageTraits, SetZero)
+{
+  ImageAccessor& image = this->GetImage();
+  
+  memset(image.GetBuffer(), 128, image.GetHeight() * image.GetWidth());
+
+  switch (image.GetFormat())
+  {
+    case PixelFormat_Grayscale8:
+    case PixelFormat_Grayscale16:
+    case PixelFormat_SignedGrayscale16:
+      ImageProcessing::Set(image, 0);
+      break;
+
+    case PixelFormat_RGB24:
+    case PixelFormat_BGRA32:
+      ImageProcessing::Set(image, 0, 0, 0, 0);
+      break;
+
+    default:
+      ASSERT_TRUE(0);
+  }
+
+  typename TestFixture::ImageTraits::PixelType zero, value;
+  TestFixture::ImageTraits::PixelTraits::SetZero(zero);
+
+  for (unsigned int y = 0; y < image.GetHeight(); y++)
+  {
+    for (unsigned int x = 0; x < image.GetWidth(); x++)
+    {
+      TestFixture::ImageTraits::GetPixel(value, image, x, y);
+      ASSERT_TRUE(TestFixture::ImageTraits::PixelTraits::IsEqual(zero, value));
+    }
+  }
+}
+
+
+TYPED_TEST(TestIntegerImageTraits, SetZeroFloat)
+{
+  ImageAccessor& image = this->GetImage();
+  
+  memset(image.GetBuffer(), 128, image.GetHeight() * image.GetWidth());
+
+  unsigned int c = 0;
+  for (unsigned int y = 0; y < image.GetHeight(); y++)
+  {
+    for (unsigned int x = 0; x < image.GetWidth(); x++, c++)
+    {
+      TestFixture::ImageTraits::SetFloatPixel(image, c, x, y);
+    }
+  }
+
+  c = 0;
+  for (unsigned int y = 0; y < image.GetHeight(); y++)
+  {
+    for (unsigned int x = 0; x < image.GetWidth(); x++, c++)
+    {
+      ASSERT_FLOAT_EQ(c, TestFixture::ImageTraits::GetFloatPixel(image, x, y));
+    }
+  }
+}
--- a/UnitTestsSources/ImageTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/ImageTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -40,9 +41,11 @@
 #include "../Core/Images/JpegWriter.h"
 #include "../Core/Images/PngReader.h"
 #include "../Core/Images/PngWriter.h"
+#include "../Core/Images/PamReader.h"
+#include "../Core/Images/PamWriter.h"
 #include "../Core/Toolbox.h"
-#include "../Core/Uuid.h"
-#include "../OrthancServer/OrthancInitialization.h"
+#include "../Core/TemporaryFile.h"
+#include "../OrthancServer/OrthancInitialization.h"  // For the FontRegistry
 
 #include <stdint.h>
 
@@ -66,10 +69,13 @@
     }
   }
 
-  w.WriteToFile("UnitTestsResults/ColorPattern.png", width, height, pitch, Orthanc::PixelFormat_RGB24, &image[0]);
+  Orthanc::ImageAccessor accessor;
+  accessor.AssignReadOnly(Orthanc::PixelFormat_RGB24, width, height, pitch, &image[0]);
+
+  w.WriteToFile("UnitTestsResults/ColorPattern.png", accessor);
 
   std::string f, md5;
-  Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/ColorPattern.png");
+  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/ColorPattern.png");
   Orthanc::Toolbox::ComputeMD5(md5, f);
   ASSERT_EQ("604e785f53c99cae6ea4584870b2c41d", md5);
 }
@@ -91,10 +97,13 @@
     }
   }
 
-  w.WriteToFile("UnitTestsResults/Gray8Pattern.png", width, height, pitch, Orthanc::PixelFormat_Grayscale8, &image[0]);
+  Orthanc::ImageAccessor accessor;
+  accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale8, width, height, pitch, &image[0]);
+
+  w.WriteToFile("UnitTestsResults/Gray8Pattern.png", accessor);
 
   std::string f, md5;
-  Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.png");
+  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.png");
   Orthanc::Toolbox::ComputeMD5(md5, f);
   ASSERT_EQ("5a9b98bea3d0a6d983980cc38bfbcdb3", md5);
 }
@@ -118,10 +127,12 @@
     }
   }
 
-  w.WriteToFile("UnitTestsResults/Gray16Pattern.png", width, height, pitch, Orthanc::PixelFormat_Grayscale16, &image[0]);
+  Orthanc::ImageAccessor accessor;
+  accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale16, width, height, pitch, &image[0]);
+  w.WriteToFile("UnitTestsResults/Gray16Pattern.png", accessor);
 
   std::string f, md5;
-  Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.png");
+  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.png");
   Orthanc::Toolbox::ComputeMD5(md5, f);
   ASSERT_EQ("0785866a08bf0a02d2eeff87f658571c", md5);
 }
@@ -145,8 +156,11 @@
     }
   }
 
+  Orthanc::ImageAccessor accessor;
+  accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale16, width, height, pitch, &image[0]);
+
   std::string s;
-  w.WriteToMemory(s, width, height, pitch, Orthanc::PixelFormat_Grayscale16, &image[0]);
+  w.WriteToMemory(s, accessor);
 
   {
     Orthanc::PngReader r;
@@ -169,8 +183,8 @@
   }
 
   {
-    Orthanc::Toolbox::TemporaryFile tmp;
-    Orthanc::Toolbox::WriteFile(s, tmp.GetPath());
+    Orthanc::TemporaryFile tmp;
+    Orthanc::SystemToolbox::WriteFile(s, tmp.GetPath());
 
     Orthanc::PngReader r2;
     r2.ReadFromFile(tmp.GetPath());
@@ -200,7 +214,7 @@
   std::string s;
 
   {
-    Orthanc::Image img(Orthanc::PixelFormat_Grayscale8, 16, 16);
+    Orthanc::Image img(Orthanc::PixelFormat_Grayscale8, 16, 16, false);
     for (unsigned int y = 0, value = 0; y < img.GetHeight(); y++)
     {
       uint8_t* p = reinterpret_cast<uint8_t*>(img.GetRow(y));
@@ -214,10 +228,10 @@
     w.WriteToFile("UnitTestsResults/hello.jpg", img);
 
     w.WriteToMemory(s, img);
-    Orthanc::Toolbox::WriteFile(s, "UnitTestsResults/hello2.jpg");
+    Orthanc::SystemToolbox::WriteFile(s, "UnitTestsResults/hello2.jpg");
 
     std::string t;
-    Orthanc::Toolbox::ReadFile(t, "UnitTestsResults/hello.jpg");
+    Orthanc::SystemToolbox::ReadFile(t, "UnitTestsResults/hello.jpg");
     ASSERT_EQ(s.size(), t.size());
     ASSERT_EQ(0, memcmp(s.c_str(), t.c_str(), s.size()));
   }
@@ -225,12 +239,12 @@
   {
     Orthanc::JpegReader r1, r2;
     r1.ReadFromFile("UnitTestsResults/hello.jpg");
-    ASSERT_EQ(16, r1.GetWidth());
-    ASSERT_EQ(16, r1.GetHeight());
+    ASSERT_EQ(16u, r1.GetWidth());
+    ASSERT_EQ(16u, r1.GetHeight());
 
     r2.ReadFromMemory(s);
-    ASSERT_EQ(16, r2.GetWidth());
-    ASSERT_EQ(16, r2.GetHeight());
+    ASSERT_EQ(16u, r2.GetWidth());
+    ASSERT_EQ(16u, r2.GetHeight());
 
     for (unsigned int y = 0; y < r1.GetHeight(); y++)
     {
@@ -247,13 +261,171 @@
 
 TEST(Font, Basic)
 {
-  Orthanc::Image s(Orthanc::PixelFormat_RGB24, 640, 480);
+  Orthanc::Image s(Orthanc::PixelFormat_RGB24, 640, 480, false);
   memset(s.GetBuffer(), 0, s.GetPitch() * s.GetHeight());
 
-  ASSERT_GE(1, Orthanc::Configuration::GetFontRegistry().GetSize());
+  ASSERT_GE(1u, Orthanc::Configuration::GetFontRegistry().GetSize());
   Orthanc::Configuration::GetFontRegistry().GetFont(0).Draw(s, "Hello world É\n\rComment ça va ?\nq", 50, 60, 255, 0, 0);
 
   Orthanc::PngWriter w;
   w.WriteToFile("UnitTestsResults/font.png", s);
 }
 
+TEST(PamWriter, ColorPattern)
+{
+  Orthanc::PamWriter w;
+  unsigned int width = 17;
+  unsigned int height = 61;
+  unsigned int pitch = width * 3;
+
+  std::vector<uint8_t> image(height * pitch);
+  for (unsigned int y = 0; y < height; y++)
+  {
+    uint8_t *p = &image[0] + y * pitch;
+    for (unsigned int x = 0; x < width; x++, p += 3)
+    {
+      p[0] = (y % 3 == 0) ? 255 : 0;
+      p[1] = (y % 3 == 1) ? 255 : 0;
+      p[2] = (y % 3 == 2) ? 255 : 0;
+    }
+  }
+
+  Orthanc::ImageAccessor accessor;
+  accessor.AssignReadOnly(Orthanc::PixelFormat_RGB24, width, height, pitch, &image[0]);
+
+  w.WriteToFile("UnitTestsResults/ColorPattern.pam", accessor);
+
+  std::string f, md5;
+  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/ColorPattern.pam");
+  Orthanc::Toolbox::ComputeMD5(md5, f);
+  ASSERT_EQ("81a3441754e88969ebbe53e69891e841", md5);
+}
+
+TEST(PamWriter, Gray8Pattern)
+{
+  Orthanc::PamWriter w;
+  int width = 17;
+  int height = 256;
+  int pitch = width;
+
+  std::vector<uint8_t> image(height * pitch);
+  for (int y = 0; y < height; y++)
+  {
+    uint8_t *p = &image[0] + y * pitch;
+    for (int x = 0; x < width; x++, p++)
+    {
+      *p = y;
+    }
+  }
+
+  Orthanc::ImageAccessor accessor;
+  accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale8, width, height, pitch, &image[0]);
+
+  w.WriteToFile("UnitTestsResults/Gray8Pattern.pam", accessor);
+
+  std::string f, md5;
+  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.pam");
+  Orthanc::Toolbox::ComputeMD5(md5, f);
+  ASSERT_EQ("7873c408d26a9d11dd1c1de5e69cc0a3", md5);
+}
+
+TEST(PamWriter, Gray16Pattern)
+{
+  Orthanc::PamWriter w;
+  int width = 256;
+  int height = 256;
+  int pitch = width * 2 + 16;
+
+  std::vector<uint8_t> image(height * pitch);
+
+  int v = 0;
+  for (int y = 0; y < height; y++)
+  {
+    uint16_t *p = reinterpret_cast<uint16_t*>(&image[0] + y * pitch);
+    for (int x = 0; x < width; x++, p++, v++)
+    {
+      *p = v;
+    }
+  }
+
+  Orthanc::ImageAccessor accessor;
+  accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale16, width, height, pitch, &image[0]);
+  w.WriteToFile("UnitTestsResults/Gray16Pattern.pam", accessor);
+
+  std::string f, md5;
+  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.pam");
+  Orthanc::Toolbox::ComputeMD5(md5, f);
+  ASSERT_EQ("b268772bf28f3b2b8520ff21c5e3dcb6", md5);
+}
+
+TEST(PamWriter, EndToEnd)
+{
+  Orthanc::PamWriter w;
+  unsigned int width = 256;
+  unsigned int height = 256;
+  unsigned int pitch = width * 2 + 16;
+
+  std::vector<uint8_t> image(height * pitch);
+
+  int v = 0;
+  for (unsigned int y = 0; y < height; y++)
+  {
+    uint16_t *p = reinterpret_cast<uint16_t*>(&image[0] + y * pitch);
+    for (unsigned int x = 0; x < width; x++, p++, v++)
+    {
+      *p = v;
+    }
+  }
+
+  Orthanc::ImageAccessor accessor;
+  accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale16, width, height, pitch, &image[0]);
+
+  std::string s;
+  w.WriteToMemory(s, accessor);
+
+  {
+    Orthanc::PamReader r;
+    r.ReadFromMemory(s);
+
+    ASSERT_EQ(r.GetFormat(), Orthanc::PixelFormat_Grayscale16);
+    ASSERT_EQ(r.GetWidth(), width);
+    ASSERT_EQ(r.GetHeight(), height);
+
+    v = 0;
+    for (unsigned int y = 0; y < height; y++)
+    {
+      const uint16_t *p = reinterpret_cast<const uint16_t*>
+        ((const uint8_t*) r.GetConstBuffer() + y * r.GetPitch());
+      ASSERT_EQ(p, r.GetConstRow(y));
+      for (unsigned int x = 0; x < width; x++, p++, v++)
+      {
+        ASSERT_EQ(v, *p);
+      }
+    }
+  }
+
+  {
+    Orthanc::TemporaryFile tmp;
+    Orthanc::SystemToolbox::WriteFile(s, tmp.GetPath());
+
+    Orthanc::PamReader r2;
+    r2.ReadFromFile(tmp.GetPath());
+
+    ASSERT_EQ(r2.GetFormat(), Orthanc::PixelFormat_Grayscale16);
+    ASSERT_EQ(r2.GetWidth(), width);
+    ASSERT_EQ(r2.GetHeight(), height);
+
+    v = 0;
+    for (unsigned int y = 0; y < height; y++)
+    {
+      const uint16_t *p = reinterpret_cast<const uint16_t*>
+        ((const uint8_t*) r2.GetConstBuffer() + y * r2.GetPitch());
+      ASSERT_EQ(p, r2.GetConstRow(y));
+      for (unsigned int x = 0; x < width; x++, p++, v++)
+      {
+        ASSERT_EQ(*p, v);
+      }
+    }
+  }
+}
+
--- a/UnitTestsSources/JpegLosslessTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/JpegLosslessTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,13 +34,13 @@
 #include "PrecompiledHeadersUnitTests.h"
 #include "gtest/gtest.h"
 
-#include "../OrthancServer/Internals/DicomImageDecoder.h"
+#include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
 
-#if ORTHANC_JPEG_LOSSLESS_ENABLED == 1
+#if ORTHANC_ENABLE_JPEG_LOSSLESS == 1
 
 #include <dcmtk/dcmdata/dcfilefo.h>
 
-#include "../OrthancServer/ParsedDicomFile.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
 #include "../Core/OrthancException.h"
 #include "../Core/Images/ImageBuffer.h"
 #include "../Core/Images/PngWriter.h"
--- a/UnitTestsSources/LuaTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/LuaTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -259,9 +260,9 @@
     ASSERT_EQ(Json::stringValue, v["List"][0]["a"].type());
     ASSERT_EQ(Json::stringValue, v["List"][0]["b"].type());
     ASSERT_EQ(Json::stringValue, v["List"][0]["c"].type());
-    ASSERT_EQ("42", v["List"][0]["a"].asString());
-    ASSERT_EQ("44.37", v["List"][0]["b"].asString());
-    ASSERT_EQ("-43", v["List"][0]["c"].asString());
+    ASSERT_FLOAT_EQ(42.0f, boost::lexical_cast<float>(v["List"][0]["a"].asString()));
+    ASSERT_FLOAT_EQ(44.37f, boost::lexical_cast<float>(v["List"][0]["b"].asString()));
+    ASSERT_FLOAT_EQ(-43.0f, boost::lexical_cast<float>(v["List"][0]["c"].asString()));
     ASSERT_EQ("test3", v["List"][1][0].asString());
     ASSERT_EQ("test1", v["List"][1][1].asString());
     ASSERT_EQ("test2", v["List"][1][2].asString());
@@ -286,9 +287,14 @@
 {
   Orthanc::LuaContext lua;
 
-#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1  
-  lua.Execute("JSON = loadstring(HttpGet('http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/JSON.lua')) ()");
-  const std::string url("http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/Product.json");
+#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1
+  // The "http://www.orthanc-server.com/downloads/third-party/" does
+  // not automatically redirect to HTTPS, so we cas use it even if the
+  // OpenSSL/HTTPS support is disabled in curl
+  const std::string BASE = "http://www.orthanc-server.com/downloads/third-party/";
+
+  lua.Execute("JSON = loadstring(HttpGet('" + BASE + "JSON.lua')) ()");
+  const std::string url(BASE + "Product.json");
 #endif
 
   std::string s;
--- a/UnitTestsSources/MemoryCacheTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/MemoryCacheTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/UnitTestsSources/MultiThreadingTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,17 +34,205 @@
 #include "PrecompiledHeadersUnitTests.h"
 #include "gtest/gtest.h"
 
-#include "../OrthancServer/Scheduler/ServerScheduler.h"
+#include "../Core/FileStorage/MemoryStorageArea.h"
+#include "../Core/JobsEngine/JobsEngine.h"
+#include "../Core/Logging.h"
+#include "../Core/MultiThreading/SharedMessageQueue.h"
 #include "../Core/OrthancException.h"
+#include "../Core/SerializationToolbox.h"
+#include "../Core/SystemToolbox.h"
 #include "../Core/Toolbox.h"
-#include "../Core/MultiThreading/Locker.h"
-#include "../Core/MultiThreading/Mutex.h"
-#include "../Core/MultiThreading/ReaderWriterLock.h"
+#include "../OrthancServer/DatabaseWrapper.h"
+#include "../OrthancServer/ServerContext.h"
+#include "../OrthancServer/ServerJobs/LuaJobManager.h"
+#include "../OrthancServer/ServerJobs/OrthancJobUnserializer.h"
+
+#include "../Core/JobsEngine/Operations/JobOperationValues.h"
+#include "../Core/JobsEngine/Operations/NullOperationValue.h"
+#include "../Core/JobsEngine/Operations/StringOperationValue.h"
+#include "../OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.h"
+
+#include "../Core/JobsEngine/Operations/LogJobOperation.h"
+#include "../OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h"
+#include "../OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h"
+#include "../OrthancServer/ServerJobs/Operations/StorePeerOperation.h"
+#include "../OrthancServer/ServerJobs/Operations/StoreScuOperation.h"
+#include "../OrthancServer/ServerJobs/Operations/SystemCallOperation.h"
+
+#include "../OrthancServer/ServerJobs/ArchiveJob.h"
+#include "../OrthancServer/ServerJobs/DicomModalityStoreJob.h"
+#include "../OrthancServer/ServerJobs/MergeStudyJob.h"
+#include "../OrthancServer/ServerJobs/OrthancPeerStoreJob.h"
+#include "../OrthancServer/ServerJobs/ResourceModificationJob.h"
+#include "../OrthancServer/ServerJobs/SplitStudyJob.h"
+
 
 using namespace Orthanc;
 
 namespace
 {
+  class DummyJob : public IJob
+  {
+  private:
+    bool         fails_;
+    unsigned int count_;
+    unsigned int steps_;
+
+  public:
+    DummyJob() :
+      fails_(false),
+      count_(0),
+      steps_(4)
+    {
+    }
+
+    explicit DummyJob(bool fails) :
+      fails_(fails),
+      count_(0),
+      steps_(4)
+    {
+    }
+
+    virtual void Start()
+    {
+    }
+
+    virtual void Reset()
+    {
+    }
+    
+    virtual JobStepResult Step()
+    {
+      if (fails_)
+      {
+        return JobStepResult::Failure(ErrorCode_ParameterOutOfRange);
+      }
+      else if (count_ == steps_ - 1)
+      {
+        return JobStepResult::Success();
+      }
+      else
+      {
+        count_++;
+        return JobStepResult::Continue();
+      }
+    }
+
+    virtual void Stop(JobStopReason reason)
+    {
+    }
+
+    virtual float GetProgress()
+    {
+      return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
+    }
+
+    virtual void GetJobType(std::string& type)
+    {
+      type = "DummyJob";
+    }
+
+    virtual bool Serialize(Json::Value& value)
+    {
+      value = Json::objectValue;
+      value["Type"] = "DummyJob";
+      return true;
+    }
+
+    virtual void GetPublicContent(Json::Value& value)
+    {
+      value["hello"] = "world";
+    }
+  };
+
+
+  class DummyInstancesJob : public SetOfInstancesJob
+  {
+  private:
+    bool   trailingStepDone_;
+    
+  protected:
+    virtual bool HandleInstance(const std::string& instance)
+    {
+      return (instance != "nope");
+    }
+
+    virtual bool HandleTrailingStep()
+    {
+      if (HasTrailingStep())
+      {
+        if (trailingStepDone_)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+        else
+        {
+          trailingStepDone_ = true;
+          return true;
+        }
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+  public:
+    DummyInstancesJob() :
+      trailingStepDone_(false)
+    {
+    }
+    
+    DummyInstancesJob(const Json::Value& value) :
+      SetOfInstancesJob(value)
+    {
+      if (HasTrailingStep())
+      {
+        trailingStepDone_ = (GetPosition() == GetCommandsCount());
+      }
+      else
+      {
+        trailingStepDone_ = false;
+      }
+    }
+
+    bool IsTrailingStepDone() const
+    {
+      return trailingStepDone_;
+    }
+    
+    virtual void Stop(JobStopReason reason)
+    {
+    }
+
+    virtual void GetJobType(std::string& s)
+    {
+      s = "DummyInstancesJob";
+    }
+  };
+
+
+  class DummyUnserializer : public GenericJobUnserializer
+  {
+  public:
+    virtual IJob* UnserializeJob(const Json::Value& value)
+    {
+      if (SerializationToolbox::ReadString(value, "Type") == "DummyInstancesJob")
+      {
+        return new DummyInstancesJob(value);
+      }
+      else if (SerializationToolbox::ReadString(value, "Type") == "DummyJob")
+      {
+        return new DummyJob;
+      }
+      else
+      {
+        return GenericJobUnserializer::UnserializeJob(value);
+      }
+    }
+  };
+
+    
   class DynamicInteger : public IDynamicObject
   {
   private:
@@ -104,156 +293,1679 @@
 }
 
 
-TEST(MultiThreading, Mutex)
+
+
+static bool CheckState(JobsRegistry& registry,
+                       const std::string& id,
+                       JobState state)
 {
-  Mutex mutex;
-  Locker locker(mutex);
+  JobState s;
+  if (registry.GetState(s, id))
+  {
+    return state == s;
+  }
+  else
+  {
+    return false;
+  }
 }
 
 
-TEST(MultiThreading, ReaderWriterLock)
+static bool CheckErrorCode(JobsRegistry& registry,
+                           const std::string& id,
+                           ErrorCode code)
 {
-  ReaderWriterLock lock;
-
+  JobInfo s;
+  if (registry.GetJobInfo(s, id))
   {
-    Locker locker1(lock.ForReader());
-    Locker locker2(lock.ForReader());
+    return code == s.GetStatus().GetErrorCode();
   }
-
+  else
   {
-    Locker locker3(lock.ForWriter());
+    return false;
   }
 }
 
 
+TEST(JobsRegistry, Priority)
+{
+  JobsRegistry registry;
 
-#include "../OrthancServer/DicomProtocol/ReusableDicomUserConnection.h"
+  std::string i1, i2, i3, i4;
+  registry.Submit(i1, new DummyJob(), 10);
+  registry.Submit(i2, new DummyJob(), 30);
+  registry.Submit(i3, new DummyJob(), 20);
+  registry.Submit(i4, new DummyJob(), 5);  
+
+  registry.SetMaxCompletedJobs(2);
+
+  std::set<std::string> id;
+  registry.ListJobs(id);
+
+  ASSERT_EQ(4u, id.size());
+  ASSERT_TRUE(id.find(i1) != id.end());
+  ASSERT_TRUE(id.find(i2) != id.end());
+  ASSERT_TRUE(id.find(i3) != id.end());
+  ASSERT_TRUE(id.find(i4) != id.end());
+
+  ASSERT_TRUE(CheckState(registry, i2, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(30, job.GetPriority());
+    ASSERT_EQ(i2, job.GetId());
+
+    ASSERT_TRUE(CheckState(registry, i2, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, i2, JobState_Failure));
+  ASSERT_TRUE(CheckState(registry, i3, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(20, job.GetPriority());
+    ASSERT_EQ(i3, job.GetId());
+
+    job.MarkSuccess();
+
+    ASSERT_TRUE(CheckState(registry, i3, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, i3, JobState_Success));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(10, job.GetPriority());
+    ASSERT_EQ(i1, job.GetId());
+  }
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(5, job.GetPriority());
+    ASSERT_EQ(i4, job.GetId());
+  }
+
+  {
+    JobsRegistry::RunningJob job(registry, 1);
+    ASSERT_FALSE(job.IsValid());
+  }
+
+  JobState s;
+  ASSERT_TRUE(registry.GetState(s, i1));
+  ASSERT_FALSE(registry.GetState(s, i2));  // Removed because oldest
+  ASSERT_FALSE(registry.GetState(s, i3));  // Removed because second oldest
+  ASSERT_TRUE(registry.GetState(s, i4));
+
+  registry.SetMaxCompletedJobs(1);  // (*)
+  ASSERT_FALSE(registry.GetState(s, i1));  // Just discarded by (*)
+  ASSERT_TRUE(registry.GetState(s, i4));
+}
+
+
+TEST(JobsRegistry, Simultaneous)
+{
+  JobsRegistry registry;
+
+  std::string i1, i2;
+  registry.Submit(i1, new DummyJob(), 20);
+  registry.Submit(i2, new DummyJob(), 10);
 
-TEST(ReusableDicomUserConnection, DISABLED_Basic)
+  ASSERT_TRUE(CheckState(registry, i1, JobState_Pending));
+  ASSERT_TRUE(CheckState(registry, i2, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job1(registry, 0);
+    JobsRegistry::RunningJob job2(registry, 0);
+
+    ASSERT_TRUE(job1.IsValid());
+    ASSERT_TRUE(job2.IsValid());
+
+    job1.MarkFailure();
+    job2.MarkSuccess();
+
+    ASSERT_TRUE(CheckState(registry, i1, JobState_Running));
+    ASSERT_TRUE(CheckState(registry, i2, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, i1, JobState_Failure));
+  ASSERT_TRUE(CheckState(registry, i2, JobState_Success));
+}
+
+
+TEST(JobsRegistry, Resubmit)
 {
-  ReusableDicomUserConnection c;
-  c.SetMillisecondsBeforeClose(200);
-  printf("START\n"); fflush(stdout);
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    job.MarkFailure();
+
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+
+    registry.Resubmit(id);
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(id, job.GetId());
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+}
+
+
+TEST(JobsRegistry, Retry)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    job.MarkRetry(0);
+
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
+  
+  registry.ScheduleRetries();
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
 
   {
-    RemoteModalityParameters remote("STORESCP", "localhost", 2000, ModalityManufacturer_Generic);
-    ReusableDicomUserConnection::Locker lock(c, "ORTHANC", remote);
-    lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676281");
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    job.MarkSuccess();
+
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+}
+
+
+TEST(JobsRegistry, PausePending)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  registry.Pause(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Pause(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resume(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+}
+
+
+TEST(JobsRegistry, PauseRunning)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    registry.Resubmit(id);
+    job.MarkPause();
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resume(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+}
+
+
+TEST(JobsRegistry, PauseRetry)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    job.MarkRetry(0);
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
   }
 
-  printf("**\n"); fflush(stdout);
-  Toolbox::USleep(1000000);
-  printf("**\n"); fflush(stdout);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
+
+  registry.Pause(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resume(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
 
   {
-    RemoteModalityParameters remote("STORESCP", "localhost", 2000, ModalityManufacturer_Generic);
-    ReusableDicomUserConnection::Locker lock(c, "ORTHANC", remote);
-    lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676277");
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+}
+
+
+TEST(JobsRegistry, Cancel)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_FALSE(registry.Cancel("nope"));
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+            
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+  
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+  
+  ASSERT_TRUE(registry.Resubmit(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+  
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
   }
 
-  Toolbox::ServerBarrier();
-  printf("DONE\n"); fflush(stdout);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+  registry.Submit(id, new DummyJob(), 10);
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(id, job.GetId());
+
+    ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+
+    job.MarkCanceled();
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Resubmit(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Pause(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Resubmit(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(id, job.GetId());
+
+    ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+
+    job.MarkRetry(500);
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
 }
 
 
 
-class Tutu : public IServerCommand
+TEST(JobsEngine, SubmitAndWait)
+{
+  JobsEngine engine;
+  engine.SetThreadSleep(10);
+  engine.SetWorkersCount(3);
+  engine.Start();
+
+  Json::Value content = Json::nullValue;
+  ASSERT_TRUE(engine.GetRegistry().SubmitAndWait(content, new DummyJob(), rand() % 10));
+  ASSERT_EQ(Json::objectValue, content.type());
+  ASSERT_EQ("world", content["hello"].asString());
+
+  content = Json::nullValue;
+  ASSERT_FALSE(engine.GetRegistry().SubmitAndWait(content, new DummyJob(true), rand() % 10));
+  ASSERT_EQ(Json::nullValue, content.type());
+
+  engine.Stop();
+}
+
+
+TEST(JobsEngine, DISABLED_SequenceOfOperationsJob)
+{
+  JobsEngine engine;
+  engine.SetThreadSleep(10);
+  engine.SetWorkersCount(3);
+  engine.Start();
+
+  std::string id;
+  SequenceOfOperationsJob* job = NULL;
+
+  {
+    std::auto_ptr<SequenceOfOperationsJob> a(new SequenceOfOperationsJob);
+    job = a.get();
+    engine.GetRegistry().Submit(id, a.release(), 0);
+  }
+
+  boost::this_thread::sleep(boost::posix_time::milliseconds(500));
+
+  {
+    SequenceOfOperationsJob::Lock lock(*job);
+    size_t i = lock.AddOperation(new LogJobOperation);
+    size_t j = lock.AddOperation(new LogJobOperation);
+    size_t k = lock.AddOperation(new LogJobOperation);
+
+    StringOperationValue a("Hello");
+    StringOperationValue b("World");
+    lock.AddInput(i, a);
+    lock.AddInput(i, b);
+    
+    lock.Connect(i, j);
+    lock.Connect(j, k);
+  }
+
+  boost::this_thread::sleep(boost::posix_time::milliseconds(2000));
+
+  engine.Stop();
+
+}
+
+
+TEST(JobsEngine, DISABLED_Lua)
+{
+  JobsEngine engine;
+  engine.SetThreadSleep(10);
+  engine.SetWorkersCount(2);
+  engine.Start();
+
+  LuaJobManager lua;
+  lua.SetMaxOperationsPerJob(5);
+  lua.SetTrailingOperationTimeout(200);
+
+  for (size_t i = 0; i < 30; i++)
+  {
+    boost::this_thread::sleep(boost::posix_time::milliseconds(150));
+
+    LuaJobManager::Lock lock(lua, engine);
+    size_t a = lock.AddLogOperation();
+    size_t b = lock.AddLogOperation();
+    size_t c = lock.AddSystemCallOperation("echo");
+    lock.AddStringInput(a, boost::lexical_cast<std::string>(i));
+    lock.AddNullInput(a);
+    lock.Connect(a, b);
+    lock.Connect(a, c);
+  }
+
+  boost::this_thread::sleep(boost::posix_time::milliseconds(2000));
+
+  engine.Stop();
+}
+
+
+static bool CheckSameJson(const Json::Value& a,
+                          const Json::Value& b)
+{
+  std::string s = a.toStyledString();
+  std::string t = b.toStyledString();
+
+  if (s == t)
+  {
+    return true;
+  }
+  else
+  {
+    LOG(ERROR) << "Expected serialization: " << s;
+    LOG(ERROR) << "Actual serialization: " << t;
+    return false;
+  }
+}
+
+
+static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
+                                         IJob& job)
 {
-private:
-  int factor_;
+  Json::Value a = 42;
+  
+  if (!job.Serialize(a))
+  {
+    return false;
+  }
+  else
+  {
+    std::auto_ptr<IJob> unserialized(unserializer.UnserializeJob(a));
+  
+    Json::Value b = 43;
+    if (unserialized->Serialize(b))
+    {
+      return (CheckSameJson(a, b));
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
+
+
+static bool CheckIdempotentSetOfInstances(IJobUnserializer& unserializer,
+                                          SetOfInstancesJob& job)
+{
+  Json::Value a = 42;
+  
+  if (!job.Serialize(a))
+  {
+    return false;
+  }
+  else
+  {
+    std::auto_ptr<SetOfInstancesJob> unserialized
+      (dynamic_cast<SetOfInstancesJob*>(unserializer.UnserializeJob(a)));
+  
+    Json::Value b = 43;
+    if (unserialized->Serialize(b))
+    {    
+      return (CheckSameJson(a, b) &&
+              job.HasTrailingStep() == unserialized->HasTrailingStep() &&
+              job.GetPosition() == unserialized->GetPosition() &&
+              job.GetInstancesCount() == unserialized->GetInstancesCount() &&
+              job.GetCommandsCount() == unserialized->GetCommandsCount());
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
+
+
+static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
+                                         IJobOperation& operation)
+{
+  Json::Value a = 42;
+  operation.Serialize(a);
+  
+  std::auto_ptr<IJobOperation> unserialized(unserializer.UnserializeOperation(a));
+  
+  Json::Value b = 43;
+  unserialized->Serialize(b);
+
+  return CheckSameJson(a, b);
+}
+
 
-public:
-  Tutu(int f) : factor_(f)
+static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
+                                         JobOperationValue& value)
+{
+  Json::Value a = 42;
+  value.Serialize(a);
+  
+  std::auto_ptr<JobOperationValue> unserialized(unserializer.UnserializeValue(a));
+  
+  Json::Value b = 43;
+  unserialized->Serialize(b);
+
+  return CheckSameJson(a, b);
+}
+
+
+TEST(JobsSerialization, BadFileFormat)
+{
+  GenericJobUnserializer unserializer;
+
+  Json::Value s;
+
+  s = Json::objectValue;
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+  s = Json::arrayValue;
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+  s = "hello";
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+  s = 42;
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+}
+
+
+TEST(JobsSerialization, JobOperationValues)
+{
+  Json::Value s;
+
   {
+    JobOperationValues values;
+    values.Append(new NullOperationValue);
+    values.Append(new StringOperationValue("hello"));
+    values.Append(new StringOperationValue("world"));
+
+    s = 42;
+    values.Serialize(s);
+  }
+
+  {
+    GenericJobUnserializer unserializer;
+    std::auto_ptr<JobOperationValues> values(JobOperationValues::Unserialize(unserializer, s));
+    ASSERT_EQ(3u, values->GetSize());
+    ASSERT_EQ(JobOperationValue::Type_Null, values->GetValue(0).GetType());
+    ASSERT_EQ(JobOperationValue::Type_String, values->GetValue(1).GetType());
+    ASSERT_EQ(JobOperationValue::Type_String, values->GetValue(2).GetType());
+
+    ASSERT_EQ("hello", dynamic_cast<const StringOperationValue&>(values->GetValue(1)).GetContent());
+    ASSERT_EQ("world", dynamic_cast<const StringOperationValue&>(values->GetValue(2)).GetContent());
+  }
+}
+
+
+TEST(JobsSerialization, GenericValues)
+{
+  GenericJobUnserializer unserializer;
+  Json::Value s;
+
+  {
+    NullOperationValue null;
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, null));
+    null.Serialize(s);
   }
 
-  virtual bool Apply(ListOfStrings& outputs,
-                     const ListOfStrings& inputs)
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+  std::auto_ptr<JobOperationValue> value;
+  value.reset(unserializer.UnserializeValue(s));
+  
+  ASSERT_EQ(JobOperationValue::Type_Null, value->GetType());
+
+  {
+    StringOperationValue str("Hello");
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, str));
+    str.Serialize(s);
+  }
+
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+  value.reset(unserializer.UnserializeValue(s));
+
+  ASSERT_EQ(JobOperationValue::Type_String, value->GetType());
+  ASSERT_EQ("Hello", dynamic_cast<StringOperationValue&>(*value).GetContent());
+}
+
+
+TEST(JobsSerialization, GenericOperations)
+{   
+  DummyUnserializer unserializer;
+  Json::Value s;
+
   {
-    for (ListOfStrings::const_iterator 
-           it = inputs.begin(); it != inputs.end(); ++it)
+    LogJobOperation operation;
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+
+  {
+    std::auto_ptr<IJobOperation> operation;
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    // Make sure that we have indeed unserialized a log operation
+    Json::Value dummy;
+    ASSERT_THROW(dynamic_cast<DeleteResourceOperation&>(*operation).Serialize(dummy), std::bad_cast);
+    dynamic_cast<LogJobOperation&>(*operation).Serialize(dummy);
+  }
+}
+
+
+TEST(JobsSerialization, GenericJobs)
+{   
+  Json::Value s;
+
+  // This tests SetOfInstancesJob
+  
+  {
+    DummyInstancesJob job;
+    job.SetDescription("description");
+    job.AddInstance("hello");
+    job.AddInstance("nope");
+    job.AddInstance("world");
+    job.SetPermissive(true);
+    ASSERT_THROW(job.Step(), OrthancException);  // Not started yet
+    ASSERT_FALSE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    job.Start();
+    ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
+    ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
+
     {
-      int a = boost::lexical_cast<int>(*it);
-      int b = factor_ * a;
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    DummyUnserializer unserializer;
+    ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+    ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+    std::auto_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
 
-      printf("%d * %d = %d\n", a, factor_, b);
+    const DummyInstancesJob& tmp = dynamic_cast<const DummyInstancesJob&>(*job);
+    ASSERT_FALSE(tmp.IsStarted());
+    ASSERT_TRUE(tmp.IsPermissive());
+    ASSERT_EQ("description", tmp.GetDescription());
+    ASSERT_EQ(3u, tmp.GetInstancesCount());
+    ASSERT_EQ(2u, tmp.GetPosition());
+    ASSERT_EQ(1u, tmp.GetFailedInstances().size());
+    ASSERT_EQ("hello", tmp.GetInstance(0));
+    ASSERT_EQ("nope", tmp.GetInstance(1));
+    ASSERT_EQ("world", tmp.GetInstance(2));
+    ASSERT_TRUE(tmp.IsFailedInstance("nope"));
+  }
+
+  // SequenceOfOperationsJob
 
-      //if (a == 84) { printf("BREAK\n"); return false; }
+  {
+    SequenceOfOperationsJob job;
+    job.SetDescription("hello");
 
-      outputs.push_back(boost::lexical_cast<std::string>(b));
+    {
+      SequenceOfOperationsJob::Lock lock(job);
+      size_t a = lock.AddOperation(new LogJobOperation);
+      size_t b = lock.AddOperation(new LogJobOperation);
+      lock.Connect(a, b);
+
+      StringOperationValue s1("hello");
+      StringOperationValue s2("world");
+      lock.AddInput(a, s1);
+      lock.AddInput(a, s2);
+      lock.SetDicomAssociationTimeout(200);
+      lock.SetTrailingOperationTimeout(300);
     }
 
-    Toolbox::USleep(30000);
+    ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
+
+    {
+      GenericJobUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job));
+    }
+    
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    GenericJobUnserializer unserializer;
+    ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+    ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+    std::auto_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    std::string tmp;
+    dynamic_cast<SequenceOfOperationsJob&>(*job).GetDescription(tmp);
+    ASSERT_EQ("hello", tmp);
+  }  
+}
+
+
+static bool IsSameTagValue(ParsedDicomFile& dicom1,
+                           ParsedDicomFile& dicom2,
+                           DicomTag tag)
+{
+  std::string a, b;
+  return (dicom1.GetTagValue(a, tag) &&
+          dicom2.GetTagValue(b, tag) &&
+          (a == b));
+}
+                       
+
+
+TEST(JobsSerialization, DicomModification)
+{   
+  Json::Value s;
 
-    return true;
+  ParsedDicomFile source(true);
+  source.Insert(DICOM_TAG_STUDY_DESCRIPTION, "Test 1", false);
+  source.Insert(DICOM_TAG_SERIES_DESCRIPTION, "Test 2", false);
+  source.Insert(DICOM_TAG_PATIENT_NAME, "Test 3", false);
+
+  std::auto_ptr<ParsedDicomFile> modified(source.Clone(true));
+
+  {
+    DicomModification modification;
+    modification.SetLevel(ResourceType_Series);
+    modification.Clear(DICOM_TAG_STUDY_DESCRIPTION);
+    modification.Remove(DICOM_TAG_SERIES_DESCRIPTION);
+    modification.Replace(DICOM_TAG_PATIENT_NAME, "Test 4", true);
+
+    modification.Apply(*modified);
+
+    s = 42;
+    modification.Serialize(s);
   }
-};
+
+  {
+    DicomModification modification(s);
+    ASSERT_EQ(ResourceType_Series, modification.GetLevel());
+    
+    std::auto_ptr<ParsedDicomFile> second(source.Clone(true));
+    modification.Apply(*second);
+
+    std::string s;
+    ASSERT_TRUE(second->GetTagValue(s, DICOM_TAG_STUDY_DESCRIPTION));
+    ASSERT_TRUE(s.empty());
+    ASSERT_FALSE(second->GetTagValue(s, DICOM_TAG_SERIES_DESCRIPTION));
+    ASSERT_TRUE(second->GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+    ASSERT_EQ("Test 4", s);
+
+    ASSERT_TRUE(IsSameTagValue(source, *modified, DICOM_TAG_STUDY_INSTANCE_UID));
+    ASSERT_TRUE(IsSameTagValue(source, *second, DICOM_TAG_STUDY_INSTANCE_UID));
+
+    ASSERT_FALSE(IsSameTagValue(source, *second, DICOM_TAG_SERIES_INSTANCE_UID));
+    ASSERT_TRUE(IsSameTagValue(*modified, *second, DICOM_TAG_SERIES_INSTANCE_UID));
+  }
+}
 
 
-static void Tata(ServerScheduler* s, ServerJob* j, bool* done)
-{
-  typedef IServerCommand::ListOfStrings  ListOfStrings;
+TEST(JobsSerialization, DicomInstanceOrigin)
+{   
+  Json::Value s;
+  std::string t;
+
+  {
+    DicomInstanceOrigin origin;
+
+    s = 42;
+    origin.Serialize(s);
+  }
 
-  while (!(*done))
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_Unknown, origin.GetRequestOrigin());
+    ASSERT_EQ("", std::string(origin.GetRemoteAetC()));
+    ASSERT_FALSE(origin.LookupRemoteIp(t));
+    ASSERT_FALSE(origin.LookupRemoteAet(t));
+    ASSERT_FALSE(origin.LookupCalledAet(t));
+    ASSERT_FALSE(origin.LookupHttpUsername(t));
+  }
+
+  {
+    DicomInstanceOrigin origin(DicomInstanceOrigin::FromDicomProtocol("host", "aet", "called"));
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_DicomProtocol, origin.GetRequestOrigin());
+    ASSERT_EQ("aet", std::string(origin.GetRemoteAetC()));
+    ASSERT_TRUE(origin.LookupRemoteIp(t));   ASSERT_EQ("host", t);
+    ASSERT_TRUE(origin.LookupRemoteAet(t));  ASSERT_EQ("aet", t);
+    ASSERT_TRUE(origin.LookupCalledAet(t));  ASSERT_EQ("called", t);
+    ASSERT_FALSE(origin.LookupHttpUsername(t));
+  }
+
   {
-    ListOfStrings l;
-    s->GetListOfJobs(l);
-    for (ListOfStrings::iterator it = l.begin(); it != l.end(); ++it)
-    {
-      printf(">> %s: %0.1f\n", it->c_str(), 100.0f * s->GetProgress(*it));
-    }
-    Toolbox::USleep(3000);
+    DicomInstanceOrigin origin(DicomInstanceOrigin::FromHttp("host", "username"));
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_RestApi, origin.GetRequestOrigin());
+    ASSERT_EQ("", std::string(origin.GetRemoteAetC()));
+    ASSERT_TRUE(origin.LookupRemoteIp(t));     ASSERT_EQ("host", t);
+    ASSERT_FALSE(origin.LookupRemoteAet(t));
+    ASSERT_FALSE(origin.LookupCalledAet(t));
+    ASSERT_TRUE(origin.LookupHttpUsername(t)); ASSERT_EQ("username", t);
+  }
+
+  {
+    DicomInstanceOrigin origin(DicomInstanceOrigin::FromLua());
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_Lua, origin.GetRequestOrigin());
+    ASSERT_FALSE(origin.LookupRemoteIp(t));
+    ASSERT_FALSE(origin.LookupRemoteAet(t));
+    ASSERT_FALSE(origin.LookupCalledAet(t));
+    ASSERT_FALSE(origin.LookupHttpUsername(t));
+  }
+
+  {
+    DicomInstanceOrigin origin(DicomInstanceOrigin::FromPlugins());
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_Plugins, origin.GetRequestOrigin());
+    ASSERT_FALSE(origin.LookupRemoteIp(t));
+    ASSERT_FALSE(origin.LookupRemoteAet(t));
+    ASSERT_FALSE(origin.LookupCalledAet(t));
+    ASSERT_FALSE(origin.LookupHttpUsername(t));
   }
 }
 
 
-TEST(MultiThreading, ServerScheduler)
+namespace
+{
+  class OrthancJobsSerialization : public testing::Test
+  {
+  private:
+    MemoryStorageArea              storage_;
+    DatabaseWrapper                db_;   // The SQLite DB is in memory
+    std::auto_ptr<ServerContext>   context_;
+    TimeoutDicomConnectionManager  manager_;
+
+  public:
+    OrthancJobsSerialization()
+    {
+      db_.Open();
+      context_.reset(new ServerContext(db_, storage_, true /* running unit tests */));
+      context_->SetupJobsEngine(true, false);
+    }
+
+    virtual ~OrthancJobsSerialization()
+    {
+      context_->Stop();
+      context_.reset(NULL);
+      db_.Close();
+    }
+
+    ServerContext& GetContext() 
+    {
+      return *context_;
+    }
+
+    bool CreateInstance(std::string& id)
+    {
+      // Create a sample DICOM file
+      ParsedDicomFile dicom(true);
+      dicom.Replace(DICOM_TAG_PATIENT_NAME, std::string("JODOGNE"),
+                    false, DicomReplaceMode_InsertIfAbsent);
+
+      DicomInstanceToStore toStore;
+      toStore.SetParsedDicomFile(dicom);
+
+      return (context_->Store(id, toStore) == StoreStatus_Success);
+    }
+  };
+}
+
+
+TEST_F(OrthancJobsSerialization, Values)
 {
-  ServerScheduler scheduler(10);
+  std::string id;
+  ASSERT_TRUE(CreateInstance(id));
+
+  Json::Value s;
+  OrthancJobUnserializer unserializer(GetContext());
+    
+  {
+    DicomInstanceOperationValue instance(GetContext(), id);
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, instance));
+    instance.Serialize(s);
+  }
+
+  std::auto_ptr<JobOperationValue> value;
+  value.reset(unserializer.UnserializeValue(s));
+  ASSERT_EQ(JobOperationValue::Type_DicomInstance, value->GetType());
+  ASSERT_EQ(id, dynamic_cast<DicomInstanceOperationValue&>(*value).GetId());
+
+  {
+    std::string content;
+    dynamic_cast<DicomInstanceOperationValue&>(*value).ReadDicom(content);
+
+    ParsedDicomFile dicom(content);
+    ASSERT_TRUE(dicom.GetTagValue(content, DICOM_TAG_PATIENT_NAME));
+    ASSERT_EQ("JODOGNE", content);
+  }
+}
+
+
+TEST_F(OrthancJobsSerialization, Operations)
+{
+  std::string id;
+  ASSERT_TRUE(CreateInstance(id));
+
+  Json::Value s;
+  OrthancJobUnserializer unserializer(GetContext()); 
+
+  // DeleteResourceOperation
+  
+  {
+    DeleteResourceOperation operation(GetContext());
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  std::auto_ptr<IJobOperation> operation;
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
 
-  ServerJob job;
-  ServerCommandInstance& f2 = job.AddCommand(new Tutu(2));
-  ServerCommandInstance& f3 = job.AddCommand(new Tutu(3));
-  ServerCommandInstance& f4 = job.AddCommand(new Tutu(4));
-  ServerCommandInstance& f5 = job.AddCommand(new Tutu(5));
-  f2.AddInput(boost::lexical_cast<std::string>(42));
-  //f3.AddInput(boost::lexical_cast<std::string>(42));
-  //f4.AddInput(boost::lexical_cast<std::string>(42));
-  f2.ConnectOutput(f3);
-  f3.ConnectOutput(f4);
-  f4.ConnectOutput(f5);
+    Json::Value dummy;
+    ASSERT_THROW(dynamic_cast<LogJobOperation&>(*operation).Serialize(dummy), std::bad_cast);
+    dynamic_cast<DeleteResourceOperation&>(*operation).Serialize(dummy);
+  }
+
+  // StorePeerOperation
+
+  {
+    WebServiceParameters peer;
+    peer.SetUrl("http://localhost/");
+    peer.SetCredentials("username", "password");
+    peer.SetPkcs11Enabled(true);
+
+    StorePeerOperation operation(peer);
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    const StorePeerOperation& tmp = dynamic_cast<StorePeerOperation&>(*operation);
+    ASSERT_EQ("http://localhost/", tmp.GetPeer().GetUrl());
+    ASSERT_EQ("username", tmp.GetPeer().GetUsername());
+    ASSERT_EQ("password", tmp.GetPeer().GetPassword());
+    ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled());
+  }
+
+  // StoreScuOperation
+
+  {
+    RemoteModalityParameters modality;
+    modality.SetApplicationEntityTitle("REMOTE");
+    modality.SetHost("192.168.1.1");
+    modality.SetPortNumber(1000);
+    modality.SetManufacturer(ModalityManufacturer_StoreScp);
+
+    StoreScuOperation operation("TEST", modality);
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
 
-  f3.SetConnectedToSink(true);
-  f5.SetConnectedToSink(true);
+    const StoreScuOperation& tmp = dynamic_cast<StoreScuOperation&>(*operation);
+    ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost());
+    ASSERT_EQ(1000, tmp.GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer());
+    ASSERT_EQ("TEST", tmp.GetLocalAet());
+  }
+
+  // SystemCallOperation
+
+  {
+    SystemCallOperation operation(std::string("echo"));
+    operation.AddPreArgument("a");
+    operation.AddPreArgument("b");
+    operation.AddPostArgument("c");
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
 
-  job.SetDescription("tutu");
+    const SystemCallOperation& tmp = dynamic_cast<SystemCallOperation&>(*operation);
+    ASSERT_EQ("echo", tmp.GetCommand());
+    ASSERT_EQ(2u, tmp.GetPreArgumentsCount());
+    ASSERT_EQ(1u, tmp.GetPostArgumentsCount());
+    ASSERT_EQ("a", tmp.GetPreArgument(0));
+    ASSERT_EQ("b", tmp.GetPreArgument(1));
+    ASSERT_EQ("c", tmp.GetPostArgument(0));
+  }
+
+  // ModifyInstanceOperation
 
-  bool done = false;
-  boost::thread t(Tata, &scheduler, &job, &done);
+  {
+    std::auto_ptr<DicomModification> modification(new DicomModification);
+    modification->SetupAnonymization(DicomVersion_2008);
+    
+    ModifyInstanceOperation operation(GetContext(), RequestOrigin_Lua, modification.release());
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    const ModifyInstanceOperation& tmp = dynamic_cast<ModifyInstanceOperation&>(*operation);
+    ASSERT_EQ(RequestOrigin_Lua, tmp.GetRequestOrigin());
+    ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION));
+  }
+}
 
 
-  //scheduler.Submit(job);
+TEST_F(OrthancJobsSerialization, Jobs)
+{
+  Json::Value s;
+
+  // ArchiveJob
+
+  {
+    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
+    ArchiveJob job(tmp, GetContext(), false, false);
+    ASSERT_FALSE(job.Serialize(s));  // Cannot serialize this
+  }
+
+  // DicomModalityStoreJob
+
+  OrthancJobUnserializer unserializer(GetContext()); 
+
+  {
+    RemoteModalityParameters modality;
+    modality.SetApplicationEntityTitle("REMOTE");
+    modality.SetHost("192.168.1.1");
+    modality.SetPortNumber(1000);
+    modality.SetManufacturer(ModalityManufacturer_StoreScp);
+
+    DicomModalityStoreJob job(GetContext());
+    job.SetLocalAet("LOCAL");
+    job.SetRemoteModality(modality);
+    job.SetMoveOriginator("MOVESCU", 42);
+
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::auto_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    DicomModalityStoreJob& tmp = dynamic_cast<DicomModalityStoreJob&>(*job);
+    ASSERT_EQ("LOCAL", tmp.GetLocalAet());
+    ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost());
+    ASSERT_EQ(1000, tmp.GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer());
+    ASSERT_TRUE(tmp.HasMoveOriginator());
+    ASSERT_EQ("MOVESCU", tmp.GetMoveOriginatorAet());
+    ASSERT_EQ(42, tmp.GetMoveOriginatorId());
+  }
+
+  // OrthancPeerStoreJob
+
+  {
+    WebServiceParameters peer;
+    peer.SetUrl("http://localhost/");
+    peer.SetCredentials("username", "password");
+    peer.SetPkcs11Enabled(true);
 
-  IServerCommand::ListOfStrings l;
-  scheduler.SubmitAndWait(l, job);
+    OrthancPeerStoreJob job(GetContext());
+    job.SetPeer(peer);
+    
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::auto_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    OrthancPeerStoreJob& tmp = dynamic_cast<OrthancPeerStoreJob&>(*job);
+    ASSERT_EQ("http://localhost/", tmp.GetPeer().GetUrl());
+    ASSERT_EQ("username", tmp.GetPeer().GetUsername());
+    ASSERT_EQ("password", tmp.GetPeer().GetPassword());
+    ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled());
+  }
+
+  // ResourceModificationJob
+
+  {
+    std::auto_ptr<DicomModification> modification(new DicomModification);
+    modification->SetupAnonymization(DicomVersion_2008);    
+
+    ResourceModificationJob job(GetContext());
+    job.SetModification(modification.release(), ResourceType_Patient, true);
+    job.SetOrigin(DicomInstanceOrigin::FromLua());
+    
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::auto_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
+    ASSERT_TRUE(tmp.IsAnonymization());
+    ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
+    ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION));
+  }
+
+  // SplitStudyJob
+
+  std::string instance;
+  ASSERT_TRUE(CreateInstance(instance));
+
+  std::string study, series;
+
+  {
+    ServerContext::DicomCacheLocker lock(GetContext(), instance);
+    study = lock.GetDicom().GetHasher().HashStudy();
+    series = lock.GetDicom().GetHasher().HashSeries();
+  }
+
+  {
+    std::list<std::string> tmp;
+    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Study);
+    ASSERT_EQ(1u, tmp.size());
+    ASSERT_EQ(study, tmp.front());
+    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Series);
+    ASSERT_EQ(1u, tmp.size());
+    ASSERT_EQ(series, tmp.front());
+  }
 
-  ASSERT_EQ(2u, l.size());
-  ASSERT_EQ(42 * 2 * 3, boost::lexical_cast<int>(l.front()));
-  ASSERT_EQ(42 * 2 * 3 * 4 * 5, boost::lexical_cast<int>(l.back()));
+  std::string study2;
+
+  {
+    std::string a, b;
+
+    {
+      ASSERT_THROW(SplitStudyJob(GetContext(), std::string("nope")), OrthancException);
+
+      SplitStudyJob job(GetContext(), study);
+      job.SetKeepSource(true);
+      job.AddSourceSeries(series);
+      ASSERT_THROW(job.AddSourceSeries("nope"), OrthancException);
+      job.SetOrigin(DicomInstanceOrigin::FromLua());
+      job.Replace(DICOM_TAG_PATIENT_NAME, "hello");
+      job.Remove(DICOM_TAG_PATIENT_BIRTH_DATE);
+      ASSERT_THROW(job.Replace(DICOM_TAG_SERIES_DESCRIPTION, "nope"), OrthancException);
+      ASSERT_THROW(job.Remove(DICOM_TAG_SERIES_DESCRIPTION), OrthancException);
+    
+      ASSERT_TRUE(job.GetTargetStudy().empty());
+      a = job.GetTargetStudyUid();
+      ASSERT_TRUE(job.LookupTargetSeriesUid(b, series));
+
+      job.AddTrailingStep();
+      job.Start();
+      ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
+      ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
+
+      study2 = job.GetTargetStudy();
+      ASSERT_FALSE(study2.empty());
+
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+      ASSERT_TRUE(job.Serialize(s));
+    }
+
+    {
+      std::auto_ptr<IJob> job;
+      job.reset(unserializer.UnserializeJob(s));
+
+      SplitStudyJob& tmp = dynamic_cast<SplitStudyJob&>(*job);
+      ASSERT_TRUE(tmp.IsKeepSource());
+      ASSERT_EQ(study, tmp.GetSourceStudy());
+      ASSERT_EQ(a, tmp.GetTargetStudyUid());
+      ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
+
+      std::string s;
+      ASSERT_EQ(study2, tmp.GetTargetStudy());
+      ASSERT_FALSE(tmp.LookupTargetSeriesUid(s, "nope"));
+      ASSERT_TRUE(tmp.LookupTargetSeriesUid(s, series));
+      ASSERT_EQ(b, s);
+
+      ASSERT_FALSE(tmp.LookupReplacement(s, DICOM_TAG_STUDY_DESCRIPTION));
+      ASSERT_TRUE(tmp.LookupReplacement(s, DICOM_TAG_PATIENT_NAME));
+      ASSERT_EQ("hello", s);
+      ASSERT_FALSE(tmp.IsRemoved(DICOM_TAG_PATIENT_NAME));
+      ASSERT_TRUE(tmp.IsRemoved(DICOM_TAG_PATIENT_BIRTH_DATE));
+    }
+  }
 
-  for (IServerCommand::ListOfStrings::iterator i = l.begin(); i != l.end(); i++)
+  {
+    std::list<std::string> tmp;
+    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Study);
+    ASSERT_EQ(2u, tmp.size());
+    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Series);
+    ASSERT_EQ(2u, tmp.size());
+  }
+
+  // MergeStudyJob
+
   {
-    printf("** %s\n", i->c_str());
+    ASSERT_THROW(SplitStudyJob(GetContext(), std::string("nope")), OrthancException);
+
+    MergeStudyJob job(GetContext(), study);
+    job.SetKeepSource(true);
+    job.AddSource(study2);
+    ASSERT_THROW(job.AddSourceSeries("nope"), OrthancException);
+    ASSERT_THROW(job.AddSourceStudy("nope"), OrthancException);
+    ASSERT_THROW(job.AddSource("nope"), OrthancException);
+    job.SetOrigin(DicomInstanceOrigin::FromLua());
+    
+    ASSERT_EQ(job.GetTargetStudy(), study);
+
+    job.AddTrailingStep();
+    job.Start();
+    ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
+    ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
+
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::list<std::string> tmp;
+    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Study);
+    ASSERT_EQ(2u, tmp.size());
+    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Series);
+    ASSERT_EQ(3u, tmp.size());
+  }
+
+  {
+    std::auto_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    MergeStudyJob& tmp = dynamic_cast<MergeStudyJob&>(*job);
+    ASSERT_TRUE(tmp.IsKeepSource());
+    ASSERT_EQ(study, tmp.GetTargetStudy());
+    ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
+  }
+}
+
+
+TEST(JobsSerialization, Registry)
+{   
+  Json::Value s;
+  std::string i1, i2;
+
+  {
+    JobsRegistry registry;
+    registry.Submit(i1, new DummyJob(), 10);
+    registry.Submit(i2, new SequenceOfOperationsJob(), 30);
+    registry.Serialize(s);
   }
 
-  //Toolbox::ServerBarrier();
-  //Toolbox::USleep(3000000);
-
-  scheduler.Stop();
+  {
+    DummyUnserializer unserializer;
+    JobsRegistry registry(unserializer, s);
 
-  done = true;
-  if (t.joinable())
-  {
-    t.join();
+    Json::Value t;
+    registry.Serialize(t);
+    ASSERT_TRUE(CheckSameJson(s, t));
   }
 }
+
+
+TEST(JobsSerialization, TrailingStep)
+{
+  {
+    Json::Value s;
+    
+    DummyInstancesJob job;
+    ASSERT_EQ(0, job.GetCommandsCount());
+    ASSERT_EQ(0, job.GetInstancesCount());
+
+    job.Start();
+    ASSERT_EQ(0, job.GetPosition());
+    ASSERT_FALSE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
+    ASSERT_EQ(1, job.GetPosition());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_THROW(job.Step(), OrthancException);
+  }
+
+  {
+    Json::Value s;
+    
+    DummyInstancesJob job;
+    job.AddInstance("hello");
+    job.AddInstance("world");
+    ASSERT_EQ(2, job.GetCommandsCount());
+    ASSERT_EQ(2, job.GetInstancesCount());
+
+    job.Start();
+    ASSERT_EQ(0, job.GetPosition());
+    ASSERT_FALSE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
+    ASSERT_EQ(1, job.GetPosition());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
+    ASSERT_EQ(2, job.GetPosition());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_THROW(job.Step(), OrthancException);
+  }
+
+  {
+    Json::Value s;
+    
+    DummyInstancesJob job;
+    ASSERT_EQ(0, job.GetInstancesCount());
+    ASSERT_EQ(0, job.GetCommandsCount());
+    job.AddTrailingStep();
+    ASSERT_EQ(0, job.GetInstancesCount());
+    ASSERT_EQ(1, job.GetCommandsCount());
+
+    job.Start(); // This adds the trailing step
+    ASSERT_EQ(0, job.GetPosition());
+    ASSERT_TRUE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
+    ASSERT_EQ(1, job.GetPosition());
+    ASSERT_TRUE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_THROW(job.Step(), OrthancException);
+  }
+
+  {
+    Json::Value s;
+    
+    DummyInstancesJob job;
+    job.AddInstance("hello");
+    ASSERT_EQ(1, job.GetInstancesCount());
+    ASSERT_EQ(1, job.GetCommandsCount());
+    job.AddTrailingStep();
+    ASSERT_EQ(1, job.GetInstancesCount());
+    ASSERT_EQ(2, job.GetCommandsCount());
+    
+    job.Start();
+    ASSERT_EQ(2, job.GetCommandsCount());
+    ASSERT_EQ(0, job.GetPosition());
+    ASSERT_TRUE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
+    ASSERT_EQ(1, job.GetPosition());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
+    ASSERT_EQ(2, job.GetPosition());
+    ASSERT_TRUE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_THROW(job.Step(), OrthancException);
+  }
+}
+
+
+TEST(JobsSerialization, RemoteModalityParameters)
+{
+  Json::Value s;
+
+  {
+    RemoteModalityParameters modality;
+    ASSERT_FALSE(modality.IsAdvancedFormatNeeded());
+    modality.Serialize(s, false);
+    ASSERT_EQ(Json::arrayValue, s.type());
+  }
+
+  {
+    RemoteModalityParameters modality(s);
+    ASSERT_EQ("ORTHANC", modality.GetApplicationEntityTitle());
+    ASSERT_EQ("127.0.0.1", modality.GetHost());
+    ASSERT_EQ(104u, modality.GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, modality.GetManufacturer());
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Echo));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Find));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Get));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Store));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move));
+  }
+
+  s = Json::nullValue;
+
+  {
+    RemoteModalityParameters modality;
+    ASSERT_FALSE(modality.IsAdvancedFormatNeeded());
+    ASSERT_THROW(modality.SetPortNumber(0), OrthancException);
+    ASSERT_THROW(modality.SetPortNumber(65535), OrthancException);
+    modality.SetApplicationEntityTitle("HELLO");
+    modality.SetHost("world");
+    modality.SetPortNumber(45);
+    modality.SetManufacturer(ModalityManufacturer_Dcm4Chee);
+    modality.Serialize(s, true);
+    ASSERT_EQ(Json::objectValue, s.type());
+  }
+
+  {
+    RemoteModalityParameters modality(s);
+    ASSERT_EQ("HELLO", modality.GetApplicationEntityTitle());
+    ASSERT_EQ("world", modality.GetHost());
+    ASSERT_EQ(45u, modality.GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Dcm4Chee, modality.GetManufacturer());
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Echo));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Find));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Get));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Store));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move));
+  }
+
+  s["Port"] = "46";
+  
+  {
+    RemoteModalityParameters modality(s);
+    ASSERT_EQ(46u, modality.GetPortNumber());
+  }
+
+  s["Port"] = -1;     ASSERT_THROW(RemoteModalityParameters m(s), OrthancException);
+  s["Port"] = 65535;  ASSERT_THROW(RemoteModalityParameters m(s), OrthancException);
+  s["Port"] = "nope"; ASSERT_THROW(RemoteModalityParameters m(s), OrthancException);
+
+  std::set<DicomRequestType> operations;
+  operations.insert(DicomRequestType_Echo);
+  operations.insert(DicomRequestType_Find);
+  operations.insert(DicomRequestType_Get);
+  operations.insert(DicomRequestType_Move);
+  operations.insert(DicomRequestType_Store);
+
+  ASSERT_EQ(5u, operations.size());
+
+  for (std::set<DicomRequestType>::const_iterator 
+         it = operations.begin(); it != operations.end(); ++it)
+  {
+    {
+      RemoteModalityParameters modality;
+      modality.SetRequestAllowed(*it, false);
+      ASSERT_TRUE(modality.IsAdvancedFormatNeeded());
+
+      modality.Serialize(s, false);
+      ASSERT_EQ(Json::objectValue, s.type());
+    }
+
+    {
+      RemoteModalityParameters modality(s);
+
+      ASSERT_FALSE(modality.IsRequestAllowed(*it));
+
+      for (std::set<DicomRequestType>::const_iterator 
+             it2 = operations.begin(); it2 != operations.end(); ++it2)
+      {
+        if (*it2 != *it)
+        {
+          ASSERT_TRUE(modality.IsRequestAllowed(*it2));
+        }
+      }
+    }
+  }
+}
--- a/UnitTestsSources/PluginsTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/PluginsTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,12 +34,13 @@
 #include "PrecompiledHeadersUnitTests.h"
 #include "gtest/gtest.h"
 
+#include "../../Core/OrthancException.h"
 #include "../Plugins/Engine/PluginsManager.h"
 
 using namespace Orthanc;
 
 
-#if ORTHANC_PLUGINS_ENABLED == 1
+#if ORTHANC_ENABLE_PLUGINS == 1
 
 TEST(SharedLibrary, Enumerations)
 {
@@ -57,15 +59,25 @@
   ASSERT_TRUE(l.HasFunction("GetVersionExW"));
   ASSERT_FALSE(l.HasFunction("world"));
 
-#elif defined(__linux) || defined(__FreeBSD_kernel__)
+#elif defined(__LSB_VERSION__)
+  // For Linux Standard Base, we use a low-level shared library coming
+  // with glibc:
+  // http://www.linuxfromscratch.org/lfs/view/6.5/chapter06/glibc.html
+  SharedLibrary l("libSegFault.so");
+  ASSERT_THROW(l.GetFunction("world"), OrthancException);
+  ASSERT_TRUE(l.GetFunction("_init") != NULL);
+  ASSERT_TRUE(l.HasFunction("_init"));
+  ASSERT_FALSE(l.HasFunction("world"));
+
+#elif defined(__linux__) || defined(__FreeBSD_kernel__)
   SharedLibrary l("libdl.so");
   ASSERT_THROW(l.GetFunction("world"), OrthancException);
   ASSERT_TRUE(l.GetFunction("dlopen") != NULL);
   ASSERT_TRUE(l.HasFunction("dlclose"));
   ASSERT_FALSE(l.HasFunction("world"));
 
-#elif defined(__FreeBSD__)
-  // dlopen() in FreeBSD is supplied by libc, libc.so is
+#elif defined(__FreeBSD__) || defined(__OpenBSD__)
+  // dlopen() in FreeBSD/OpenBSD is supplied by libc, libc.so is
   // a ldscript, so we can't actually use it. Use thread
   // library instead - if it works - dlopen() is good.
   SharedLibrary l("libpthread.so");
--- a/UnitTestsSources/PrecompiledHeadersUnitTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/PrecompiledHeadersUnitTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/UnitTestsSources/PrecompiledHeadersUnitTests.h	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/PrecompiledHeadersUnitTests.h	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/UnitTestsSources/RestApiTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/RestApiTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,15 +35,18 @@
 #include "gtest/gtest.h"
 
 #include <ctype.h>
+#include <boost/lexical_cast.hpp>
+#include <algorithm>
 
 #include "../Core/ChunkedBuffer.h"
 #include "../Core/HttpClient.h"
 #include "../Core/Logging.h"
+#include "../Core/SystemToolbox.h"
 #include "../Core/RestApi/RestApi.h"
-#include "../Core/Uuid.h"
 #include "../Core/OrthancException.h"
 #include "../Core/Compression/ZlibCompressor.h"
 #include "../Core/RestApi/RestApiHierarchy.h"
+#include "../Core/HttpServer/HttpContentNegociation.h"
 
 using namespace Orthanc;
 
@@ -62,15 +66,22 @@
   ASSERT_FALSE(c.IsVerbose());
 
 #if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1
+  // The "http://www.orthanc-server.com/downloads/third-party/" does
+  // not automatically redirect to HTTPS, so we cas use it even if the
+  // OpenSSL/HTTPS support is disabled in curl
+  const std::string BASE = "http://www.orthanc-server.com/downloads/third-party/";
+
   Json::Value v;
-  c.SetUrl("http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/Configuration.json");
+  c.SetUrl(BASE + "Product.json");
+
   c.Apply(v);
-  ASSERT_TRUE(v.isMember("StorageDirectory"));
+  ASSERT_TRUE(v.type() == Json::objectValue);
+  ASSERT_TRUE(v.isMember("Description"));
 #endif
 }
 
 
-#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 && ORTHANC_SSL_ENABLED == 1
+#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 && ORTHANC_ENABLE_SSL == 1
 
 /**
    The HTTPS CA certificates for BitBucket were extracted as follows:
@@ -97,12 +108,12 @@
 
 TEST(HttpClient, Ssl)
 {
-  Toolbox::WriteFile(BITBUCKET_CERTIFICATES, "UnitTestsResults/bitbucket.cert");
+  SystemToolbox::WriteFile(BITBUCKET_CERTIFICATES, "UnitTestsResults/bitbucket.cert");
 
   /*{
     std::string s;
-    Toolbox::ReadFile(s, "/usr/share/ca-certificates/mozilla/WoSign.crt");
-    Toolbox::WriteFile(s, "UnitTestsResults/bitbucket.cert");
+    SystemToolbox::ReadFile(s, "/usr/share/ca-certificates/mozilla/WoSign.crt");
+    SystemToolbox::WriteFile(s, "UnitTestsResults/bitbucket.cert");
     }*/
 
   HttpClient c;
@@ -277,7 +288,7 @@
                        const IHttpHandler::Arguments& components,
                        const UriComponents& trailing)
     {
-      return resource.Handle(*reinterpret_cast<RestApiGetCall*>(NULL));
+      return resource.Handle(*(RestApiGetCall*) NULL);
     }
   };
 }
@@ -335,3 +346,300 @@
   ASSERT_TRUE(HandleGet(root, "/hello2/a/b"));
   ASSERT_EQ(testValue, 4);
 }
+
+
+
+
+
+namespace
+{
+  class AcceptHandler : public Orthanc::HttpContentNegociation::IHandler
+  {
+  private:
+    std::string type_;
+    std::string subtype_;
+
+  public:
+    AcceptHandler()
+    {
+      Reset();
+    }
+
+    void Reset()
+    {
+      Handle("nope", "nope");
+    }
+
+    const std::string& GetType() const
+    {
+      return type_;
+    }
+
+    const std::string& GetSubType() const
+    {
+      return subtype_;
+    }
+
+    virtual void Handle(const std::string& type,
+                        const std::string& subtype)
+    {
+      type_ = type;
+      subtype_ = subtype;
+    }
+  };
+}
+
+
+TEST(RestApi, HttpContentNegociation)
+{
+  // Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
+
+  AcceptHandler h;
+
+  {
+    Orthanc::HttpContentNegociation d;
+    d.Register("audio/mp3", h);
+    d.Register("audio/basic", h);
+
+    ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/basic"));
+    ASSERT_EQ("audio", h.GetType());
+    ASSERT_EQ("basic", h.GetSubType());
+
+    ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/nope"));
+    ASSERT_EQ("audio", h.GetType());
+    ASSERT_EQ("mp3", h.GetSubType());
+    
+    ASSERT_FALSE(d.Apply("application/*; q=0.2, application/pdf"));
+    
+    ASSERT_TRUE(d.Apply("*/*; application/*; q=0.2, application/pdf"));
+    ASSERT_EQ("audio", h.GetType());
+  }
+
+  // "This would be interpreted as "text/html and text/x-c are the
+  // preferred media types, but if they do not exist, then send the
+  // text/x-dvi entity, and if that does not exist, send the
+  // text/plain entity.""
+  const std::string T1 = "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c";
+  
+  {
+    Orthanc::HttpContentNegociation d;
+    d.Register("text/plain", h);
+    d.Register("text/html", h);
+    d.Register("text/x-dvi", h);
+    ASSERT_TRUE(d.Apply(T1));
+    ASSERT_EQ("text", h.GetType());
+    ASSERT_EQ("html", h.GetSubType());
+  }
+  
+  {
+    Orthanc::HttpContentNegociation d;
+    d.Register("text/plain", h);
+    d.Register("text/x-dvi", h);
+    d.Register("text/x-c", h);
+    ASSERT_TRUE(d.Apply(T1));
+    ASSERT_EQ("text", h.GetType());
+    ASSERT_EQ("x-c", h.GetSubType());
+  }
+  
+  {
+    Orthanc::HttpContentNegociation d;
+    d.Register("text/plain", h);
+    d.Register("text/x-dvi", h);
+    d.Register("text/x-c", h);
+    d.Register("text/html", h);
+    ASSERT_TRUE(d.Apply(T1));
+    ASSERT_EQ("text", h.GetType());
+    ASSERT_TRUE(h.GetSubType() == "x-c" || h.GetSubType() == "html");
+  }
+  
+  {
+    Orthanc::HttpContentNegociation d;
+    d.Register("text/plain", h);
+    d.Register("text/x-dvi", h);
+    ASSERT_TRUE(d.Apply(T1));
+    ASSERT_EQ("text", h.GetType());
+    ASSERT_EQ("x-dvi", h.GetSubType());
+  }
+  
+  {
+    Orthanc::HttpContentNegociation d;
+    d.Register("text/plain", h);
+    ASSERT_TRUE(d.Apply(T1));
+    ASSERT_EQ("text", h.GetType());
+    ASSERT_EQ("plain", h.GetSubType());
+  }
+}
+
+
+TEST(WebServiceParameters, Serialization)
+{
+  {
+    Json::Value v = Json::arrayValue;
+    v.append("http://localhost:8042/");
+
+    WebServiceParameters p(v);
+    ASSERT_FALSE(p.IsAdvancedFormatNeeded());
+
+    Json::Value v2;
+    p.Serialize(v2, false, true);
+    ASSERT_EQ(v, v2);
+
+    WebServiceParameters p2(v2);
+    ASSERT_EQ("http://localhost:8042/", p2.GetUrl());
+    ASSERT_TRUE(p2.GetUsername().empty());
+    ASSERT_TRUE(p2.GetPassword().empty());
+    ASSERT_TRUE(p2.GetCertificateFile().empty());
+    ASSERT_TRUE(p2.GetCertificateKeyFile().empty());
+    ASSERT_TRUE(p2.GetCertificateKeyPassword().empty());
+    ASSERT_FALSE(p2.IsPkcs11Enabled());
+  }
+
+  {
+    Json::Value v = Json::arrayValue;
+    v.append("http://localhost:8042/");
+    v.append("user");
+    v.append("pass");
+
+    WebServiceParameters p(v);
+    ASSERT_FALSE(p.IsAdvancedFormatNeeded());
+    ASSERT_EQ("http://localhost:8042/", p.GetUrl());
+    ASSERT_EQ("user", p.GetUsername());
+    ASSERT_EQ("pass", p.GetPassword());
+    ASSERT_TRUE(p.GetCertificateFile().empty());
+    ASSERT_TRUE(p.GetCertificateKeyFile().empty());
+    ASSERT_TRUE(p.GetCertificateKeyPassword().empty());
+    ASSERT_FALSE(p.IsPkcs11Enabled());
+
+    Json::Value v2;
+    p.Serialize(v2, false, true);
+    ASSERT_EQ(v, v2);
+
+    p.Serialize(v2, false, false /* no password */);
+    WebServiceParameters p2(v2);
+    ASSERT_EQ(Json::arrayValue, v2.type());
+    ASSERT_EQ(3u, v2.size());
+    ASSERT_EQ("http://localhost:8042/", v2[0u].asString());
+    ASSERT_EQ("user", v2[1u].asString());
+    ASSERT_TRUE(v2[2u].asString().empty());
+  }
+
+  {
+    Json::Value v = Json::arrayValue;
+    v.append("http://localhost:8042/");
+
+    WebServiceParameters p(v);
+    ASSERT_FALSE(p.IsAdvancedFormatNeeded());
+    p.SetPkcs11Enabled(true);
+    ASSERT_TRUE(p.IsAdvancedFormatNeeded());
+
+    Json::Value v2;
+    p.Serialize(v2, false, true);
+    WebServiceParameters p2(v2);
+
+    ASSERT_EQ(Json::objectValue, v2.type());
+    ASSERT_EQ(3u, v2.size());
+    ASSERT_EQ("http://localhost:8042/", v2["Url"].asString());
+    ASSERT_TRUE(v2["Pkcs11"].asBool());
+    ASSERT_EQ(Json::objectValue, v2["HttpHeaders"].type());
+    ASSERT_EQ(0u, v2["HttpHeaders"].size());
+  }
+
+  {
+    Json::Value v = Json::arrayValue;
+    v.append("http://localhost:8042/");
+
+    WebServiceParameters p(v);
+    ASSERT_FALSE(p.IsAdvancedFormatNeeded());
+    p.SetClientCertificate("a", "b", "c");
+    ASSERT_TRUE(p.IsAdvancedFormatNeeded());
+
+    Json::Value v2;
+    p.Serialize(v2, false, true);
+    WebServiceParameters p2(v2);
+
+    ASSERT_EQ(Json::objectValue, v2.type());
+    ASSERT_EQ(6u, v2.size());
+    ASSERT_EQ("http://localhost:8042/", v2["Url"].asString());
+    ASSERT_EQ("a", v2["CertificateFile"].asString());
+    ASSERT_EQ("b", v2["CertificateKeyFile"].asString());
+    ASSERT_EQ("c", v2["CertificateKeyPassword"].asString());
+    ASSERT_FALSE(v2["Pkcs11"].asBool());
+    ASSERT_EQ(Json::objectValue, v2["HttpHeaders"].type());
+    ASSERT_EQ(0u, v2["HttpHeaders"].size());
+  }
+
+  {
+    Json::Value v = Json::arrayValue;
+    v.append("http://localhost:8042/");
+
+    WebServiceParameters p(v);
+    ASSERT_FALSE(p.IsAdvancedFormatNeeded());
+    p.AddHttpHeader("a", "b");
+    p.AddHttpHeader("c", "d");
+    ASSERT_TRUE(p.IsAdvancedFormatNeeded());
+
+    Json::Value v2;
+    p.Serialize(v2, false, true);
+    WebServiceParameters p2(v2);
+
+    ASSERT_EQ(Json::objectValue, v2.type());
+    ASSERT_EQ(3u, v2.size());
+    ASSERT_EQ("http://localhost:8042/", v2["Url"].asString());
+    ASSERT_FALSE(v2["Pkcs11"].asBool());
+    ASSERT_EQ(Json::objectValue, v2["HttpHeaders"].type());
+    ASSERT_EQ(2u, v2["HttpHeaders"].size());
+    ASSERT_EQ("b", v2["HttpHeaders"]["a"].asString());
+    ASSERT_EQ("d", v2["HttpHeaders"]["c"].asString());
+
+    std::set<std::string> a;
+    p2.ListHttpHeaders(a);
+    ASSERT_EQ(2u, a.size());
+    ASSERT_TRUE(a.find("a") != a.end());
+    ASSERT_TRUE(a.find("c") != a.end());
+
+    std::string s;
+    ASSERT_TRUE(p2.LookupHttpHeader(s, "a")); ASSERT_EQ("b", s);
+    ASSERT_TRUE(p2.LookupHttpHeader(s, "c")); ASSERT_EQ("d", s);
+    ASSERT_FALSE(p2.LookupHttpHeader(s, "nope"));
+  }
+}
+
+
+TEST(WebServiceParameters, UserProperties)
+{
+  Json::Value v = Json::nullValue;
+
+  {
+    WebServiceParameters p;
+    p.SetUrl("http://localhost:8042/");
+    ASSERT_FALSE(p.IsAdvancedFormatNeeded());
+
+    ASSERT_THROW(p.AddUserProperty("Url", "nope"), OrthancException);
+    p.AddUserProperty("Hello", "world");
+    p.AddUserProperty("a", "b");
+    ASSERT_TRUE(p.IsAdvancedFormatNeeded());
+
+    p.Serialize(v, false, true);
+
+    p.ClearUserProperties();
+    ASSERT_FALSE(p.IsAdvancedFormatNeeded());
+  }
+
+  {
+    WebServiceParameters p(v);
+    ASSERT_TRUE(p.IsAdvancedFormatNeeded());
+    ASSERT_TRUE(p.GetHttpHeaders().empty());
+
+    std::set<std::string> tmp;
+    p.ListUserProperties(tmp);
+    ASSERT_EQ(2u, tmp.size());
+    ASSERT_TRUE(tmp.find("a")     != tmp.end());
+    ASSERT_TRUE(tmp.find("Hello") != tmp.end());
+    ASSERT_TRUE(tmp.find("hello") == tmp.end());
+
+    std::string s;
+    ASSERT_TRUE(p.LookupUserProperty(s, "a"));      ASSERT_TRUE(s == "b");
+    ASSERT_TRUE(p.LookupUserProperty(s, "Hello"));  ASSERT_TRUE(s == "world");
+    ASSERT_FALSE(p.LookupUserProperty(s, "hello"));
+  }
+}
--- a/UnitTestsSources/SQLiteChromiumTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/SQLiteChromiumTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -50,35 +51,38 @@
  ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/connection_unittest.cc
  ********************************************************************/
 
-class SQLConnectionTest : public testing::Test 
+namespace
 {
-public:
-  SQLConnectionTest()
-  {
-  }
-
-  virtual ~SQLConnectionTest()
-  {
-  }
-
-  virtual void SetUp() 
+  class SQLConnectionTest : public testing::Test 
   {
-    db_.OpenInMemory();
-  }
+  public:
+    SQLConnectionTest()
+    {
+    }
 
-  virtual void TearDown() 
-  {
-    db_.Close();
-  }
+    virtual ~SQLConnectionTest()
+    {
+    }
+
+    virtual void SetUp() 
+    {
+      db_.OpenInMemory();
+    }
 
-  Connection& db() 
-  { 
-    return db_; 
-  }
+    virtual void TearDown() 
+    {
+      db_.Close();
+    }
 
-private:
-  Connection db_;
-};
+    Connection& db() 
+    { 
+      return db_; 
+    }
+
+  private:
+    Connection db_;
+  };
+}
 
 
 
@@ -265,23 +269,26 @@
  ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/transaction_unittest.cc
  ********************************************************************/
 
-class SQLTransactionTest : public SQLConnectionTest
+namespace
 {
-public:
-  virtual void SetUp()
+  class SQLTransactionTest : public SQLConnectionTest
   {
-    SQLConnectionTest::SetUp();
-    ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-  }
+  public:
+    virtual void SetUp()
+    {
+      SQLConnectionTest::SetUp();
+      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+    }
 
-  // Returns the number of rows in table "foo".
-  int CountFoo() 
-  {
-    Statement count(db(), "SELECT count(*) FROM foo");
-    count.Step();
-    return count.ColumnInt(0);
-  }
-};
+    // Returns the number of rows in table "foo".
+    int CountFoo() 
+    {
+      Statement count(db(), "SELECT count(*) FROM foo");
+      count.Step();
+      return count.ColumnInt(0);
+    }
+  };
+}
 
 
 TEST_F(SQLTransactionTest, Commit) {
--- a/UnitTestsSources/SQLiteTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/SQLiteTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,7 +34,7 @@
 #include "PrecompiledHeadersUnitTests.h"
 #include "gtest/gtest.h"
 
-#include "../Core/Toolbox.h"
+#include "../Core/SystemToolbox.h"
 #include "../Core/SQLite/Connection.h"
 #include "../Core/SQLite/Statement.h"
 #include "../Core/SQLite/Transaction.h"
@@ -58,7 +59,7 @@
 
 TEST(SQLite, Connection)
 {
-  Toolbox::RemoveFile("UnitTestsResults/coucou");
+  SystemToolbox::RemoveFile("UnitTestsResults/coucou");
   SQLite::Connection c;
   c.Open("UnitTestsResults/coucou");
   c.Execute("CREATE TABLE c(k INTEGER PRIMARY KEY AUTOINCREMENT, v INTEGER)");
--- a/UnitTestsSources/ServerIndexTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/ServerIndexTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,8 +35,8 @@
 #include "gtest/gtest.h"
 
 #include "../Core/FileStorage/FilesystemStorage.h"
+#include "../Core/FileStorage/MemoryStorageArea.h"
 #include "../Core/Logging.h"
-#include "../Core/Uuid.h"
 #include "../OrthancServer/DatabaseWrapper.h"
 #include "../OrthancServer/ServerContext.h"
 #include "../OrthancServer/ServerIndex.h"
@@ -671,11 +672,13 @@
 {
   const std::string path = "UnitTestsStorage";
 
-  Toolbox::RemoveFile(path + "/index");
+  SystemToolbox::RemoveFile(path + "/index");
   FilesystemStorage storage(path);
   DatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
-  ServerContext context(db, storage);
+  ServerContext context(db, storage, true /* running unit tests */);
+  context.SetupJobsEngine(true, false);
+
   ServerIndex& index = context.GetIndex();
 
   ASSERT_EQ(1u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
@@ -769,11 +772,12 @@
 {
   const std::string path = "UnitTestsStorage";
 
-  Toolbox::RemoveFile(path + "/index");
+  SystemToolbox::RemoveFile(path + "/index");
   FilesystemStorage storage(path);
   DatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
-  ServerContext context(db, storage);
+  ServerContext context(db, storage, true /* running unit tests */);
+  context.SetupJobsEngine(true, false);
   ServerIndex& index = context.GetIndex();
 
   index.SetMaximumStorageSize(10);
@@ -790,17 +794,26 @@
   {
     std::string id = boost::lexical_cast<std::string>(i);
     DicomMap instance;
-    instance.SetValue(DICOM_TAG_PATIENT_ID, "patient-" + id);
-    instance.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "study-" + id);
-    instance.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "series-" + id);
-    instance.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "instance-" + id);
+    instance.SetValue(DICOM_TAG_PATIENT_ID, "patient-" + id, false);
+    instance.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "study-" + id, false);
+    instance.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "series-" + id, false);
+    instance.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "instance-" + id, false);
+    instance.SetValue(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.1", false);  // CR image
 
     std::map<MetadataType, std::string> instanceMetadata;
-    ServerIndex::MetadataMap metadata;
-    ASSERT_EQ(StoreStatus_Success, index.Store(instanceMetadata, instance, attachments, "", metadata));
-    ASSERT_EQ(2u, instanceMetadata.size());
+    DicomInstanceToStore toStore;
+    toStore.SetSummary(instance);
+    ASSERT_EQ(StoreStatus_Success, index.Store(instanceMetadata, toStore, attachments));
+    ASSERT_EQ(5u, instanceMetadata.size());
     ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_RemoteAet) != instanceMetadata.end());
     ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_ReceptionDate) != instanceMetadata.end());
+    ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_TransferSyntax) != instanceMetadata.end());
+    ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_SopClassUid) != instanceMetadata.end());
+
+    // By default, an Explicit VR Little Endian is used by Orthanc
+    ASSERT_EQ("1.2.840.10008.1.2.1", instanceMetadata[MetadataType_Instance_TransferSyntax]);
+
+    ASSERT_EQ("1.2.840.10008.5.1.4.1.1.1", instanceMetadata[MetadataType_Instance_SopClassUid]);
 
     DicomInstanceHasher hasher(instance);
     ids.push_back(hasher.HashPatient());
@@ -823,8 +836,144 @@
   }
 
   // Because the DB is in memory, the SQLite index must not have been created
-  ASSERT_THROW(Toolbox::GetFileSize(path + "/index"), OrthancException);  
+  ASSERT_FALSE(SystemToolbox::IsRegularFile(path + "/index"));
 
   context.Stop();
   db.Close();
 }
+
+
+TEST(LookupIdentifierQuery, NormalizeIdentifier)
+{
+  ASSERT_EQ("H^L.LO", ServerToolbox::NormalizeIdentifier("   Hé^l.LO  %_  "));
+  ASSERT_EQ("1.2.840.113619.2.176.2025", ServerToolbox::NormalizeIdentifier("   1.2.840.113619.2.176.2025  "));
+}
+
+
+TEST(ServerIndex, Overwrite)
+{
+  for (unsigned int i = 0; i < 2; i++)
+  {
+    bool overwrite = (i == 0);
+
+    MemoryStorageArea storage;
+    DatabaseWrapper db;   // The SQLite DB is in memory
+    db.Open();
+    ServerContext context(db, storage, true /* running unit tests */);
+    context.SetupJobsEngine(true, false);
+    context.SetCompressionEnabled(true);
+
+    DicomMap instance;
+    instance.SetValue(DICOM_TAG_PATIENT_ID, "patient", false);
+    instance.SetValue(DICOM_TAG_PATIENT_NAME, "name", false);
+    instance.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "study", false);
+    instance.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "series", false);
+    instance.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "sop", false);
+    instance.SetValue(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.1", false);  // CR image
+
+    DicomInstanceHasher hasher(instance);
+    std::string id = hasher.HashInstance();
+    context.GetIndex().SetOverwriteInstances(overwrite);
+
+    Json::Value tmp;
+    context.GetIndex().ComputeStatistics(tmp);
+    ASSERT_EQ(0, tmp["CountInstances"].asInt());
+    ASSERT_EQ(0, boost::lexical_cast<int>(tmp["TotalDiskSize"].asString()));
+
+    {
+      DicomInstanceToStore toStore;
+      toStore.SetSummary(instance);
+      toStore.SetOrigin(DicomInstanceOrigin::FromPlugins());
+
+      std::string id2;
+      ASSERT_EQ(StoreStatus_Success, context.Store(id2, toStore));
+      ASSERT_EQ(id, id2);
+    }
+
+    FileInfo dicom1, json1;
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, id, FileContentType_Dicom));
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(json1, id, FileContentType_DicomAsJson));
+
+    context.GetIndex().ComputeStatistics(tmp);
+    ASSERT_EQ(1, tmp["CountInstances"].asInt());
+    ASSERT_EQ(dicom1.GetCompressedSize() + json1.GetCompressedSize(),
+              boost::lexical_cast<size_t>(tmp["TotalDiskSize"].asString()));
+    ASSERT_EQ(dicom1.GetUncompressedSize() + json1.GetUncompressedSize(),
+              boost::lexical_cast<size_t>(tmp["TotalUncompressedSize"].asString()));
+
+    context.ReadDicomAsJson(tmp, id);
+    ASSERT_EQ("name", tmp["0010,0010"]["Value"].asString());
+    
+    {
+      ServerContext::DicomCacheLocker locker(context, id);
+      std::string tmp;
+      locker.GetDicom().GetTagValue(tmp, DICOM_TAG_PATIENT_NAME);
+      ASSERT_EQ("name", tmp);
+    }
+
+    {
+      DicomMap instance2;
+      instance2.Assign(instance);
+      instance2.SetValue(DICOM_TAG_PATIENT_NAME, "overwritten", false);
+
+      DicomInstanceToStore toStore;
+      toStore.SetSummary(instance2);
+      toStore.SetOrigin(DicomInstanceOrigin::FromPlugins());
+
+      std::string id2;
+      ASSERT_EQ(overwrite ? StoreStatus_Success : StoreStatus_AlreadyStored, context.Store(id2, toStore));
+      ASSERT_EQ(id, id2);
+    }
+
+    FileInfo dicom2, json2;
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, id, FileContentType_Dicom));
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(json2, id, FileContentType_DicomAsJson));
+
+    context.GetIndex().ComputeStatistics(tmp);
+    ASSERT_EQ(1, tmp["CountInstances"].asInt());
+    ASSERT_EQ(dicom2.GetCompressedSize() + json2.GetCompressedSize(),
+              boost::lexical_cast<size_t>(tmp["TotalDiskSize"].asString()));
+    ASSERT_EQ(dicom2.GetUncompressedSize() + json2.GetUncompressedSize(),
+              boost::lexical_cast<size_t>(tmp["TotalUncompressedSize"].asString()));
+
+    if (overwrite)
+    {
+      ASSERT_NE(dicom1.GetUuid(), dicom2.GetUuid());
+      ASSERT_NE(json1.GetUuid(), json2.GetUuid());
+      ASSERT_NE(dicom1.GetUncompressedSize(), dicom2.GetUncompressedSize());
+      ASSERT_NE(json1.GetUncompressedSize(), json2.GetUncompressedSize());
+    
+      context.ReadDicomAsJson(tmp, id);
+      ASSERT_EQ("overwritten", tmp["0010,0010"]["Value"].asString());
+    
+      {
+        ServerContext::DicomCacheLocker locker(context, id);
+        std::string tmp;
+        locker.GetDicom().GetTagValue(tmp, DICOM_TAG_PATIENT_NAME);
+        ASSERT_EQ("overwritten", tmp);
+      }
+    }
+    else
+    {
+      ASSERT_EQ(dicom1.GetUuid(), dicom2.GetUuid());
+      ASSERT_EQ(json1.GetUuid(), json2.GetUuid());
+      ASSERT_EQ(dicom1.GetUncompressedSize(), dicom2.GetUncompressedSize());
+      ASSERT_EQ(json1.GetUncompressedSize(), json2.GetUncompressedSize());
+
+      context.ReadDicomAsJson(tmp, id);
+      ASSERT_EQ("name", tmp["0010,0010"]["Value"].asString());
+    
+      {
+        ServerContext::DicomCacheLocker locker(context, id);
+        std::string tmp;
+        locker.GetDicom().GetTagValue(tmp, DICOM_TAG_PATIENT_NAME);
+        ASSERT_EQ("name", tmp);
+      }
+    }
+
+    context.Stop();
+    db.Close();
+  }
+}
+
+
--- a/UnitTestsSources/StreamTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/StreamTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,9 +34,9 @@
 #include "PrecompiledHeadersUnitTests.h"
 #include "gtest/gtest.h"
 
+#include "../Core/SystemToolbox.h"
 #include "../Core/Toolbox.h"
 #include "../Core/OrthancException.h"
-#include "../Core/Uuid.h"
 #include "../Core/HttpServer/BufferHttpSender.h"
 #include "../Core/HttpServer/FilesystemHttpSender.h"
 #include "../Core/HttpServer/HttpStreamTranscoder.h"
@@ -74,7 +75,7 @@
 
   std::string uncompressed;
   IBufferCompressor::Uncompress(uncompressed, c, compressed);
-  ASSERT_EQ(0, uncompressed.size());
+  ASSERT_TRUE(uncompressed.empty());
 }
 
 
@@ -107,7 +108,7 @@
 
   std::string uncompressed;
   IBufferCompressor::Uncompress(uncompressed, c, compressed);
-  ASSERT_EQ(0, uncompressed.size());
+  ASSERT_TRUE(uncompressed.empty());
 }
 
 
@@ -154,6 +155,7 @@
   ZlibCompressor c;
   IBufferCompressor::Compress(compressed, c, s);
 
+  ASSERT_FALSE(compressed.empty());
   compressed[compressed.size() - 1] = 'a';
   std::string u;
 
@@ -172,7 +174,7 @@
 
   std::string uncompressed;
   IBufferCompressor::Uncompress(uncompressed, c, compressed);
-  ASSERT_EQ(0u, uncompressed.size());
+  ASSERT_TRUE(uncompressed.empty());
 }
 
 
@@ -232,14 +234,14 @@
   std::string t;
 
   {
-    Toolbox::WriteFile(s, path);
+    SystemToolbox::WriteFile(s, path);
     FilesystemHttpSender sender(path);
     ASSERT_TRUE(ReadAllStream(t, sender));
     ASSERT_EQ(s, t);
   }
 
   {
-    Toolbox::WriteFile("", path);
+    SystemToolbox::WriteFile("", path);
     FilesystemHttpSender sender(path);
     ASSERT_TRUE(ReadAllStream(t, sender));
     ASSERT_EQ(0u, t.size());
--- a/UnitTestsSources/UnitTestsMain.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/UnitTestsMain.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -41,8 +42,8 @@
 #include "../Core/HttpServer/HttpToolbox.h"
 #include "../Core/Logging.h"
 #include "../Core/OrthancException.h"
+#include "../Core/TemporaryFile.h"
 #include "../Core/Toolbox.h"
-#include "../Core/Uuid.h"
 #include "../OrthancServer/OrthancInitialization.h"
 
 
@@ -364,12 +365,18 @@
   std::string decoded;
   Toolbox::DecodeBase64(decoded, hello);
   ASSERT_EQ("Hello world", decoded);
+
+  // Invalid character
+  ASSERT_THROW(Toolbox::DecodeBase64(decoded, "?"), OrthancException);
+
+  // All the allowed characters
+  Toolbox::DecodeBase64(decoded, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=");
 }
 
 TEST(Toolbox, PathToExecutable)
 {
-  printf("[%s]\n", Toolbox::GetPathToExecutable().c_str());
-  printf("[%s]\n", Toolbox::GetDirectoryOfExecutable().c_str());
+  printf("[%s]\n", SystemToolbox::GetPathToExecutable().c_str());
+  printf("[%s]\n", SystemToolbox::GetDirectoryOfExecutable().c_str());
 }
 
 TEST(Toolbox, StripSpaces)
@@ -410,10 +417,6 @@
   // This is a Latin-1 test string
   const unsigned char data[10] = { 0xe0, 0xe9, 0xea, 0xe7, 0x26, 0xc6, 0x61, 0x62, 0x63, 0x00 };
   
-  /*FILE* f = fopen("/tmp/tutu", "w");
-  fwrite(&data[0], 9, 1, f);
-  fclose(f);*/
-
   std::string s((char*) &data[0], 10);
   ASSERT_EQ("&abc", Toolbox::ConvertToAscii(s));
 
@@ -458,7 +461,21 @@
 }
 
 
-#if defined(__linux)
+TEST(Toolbox, IsAsciiString)
+{
+  std::string s = "Hello 12 /";
+  ASSERT_EQ(10u, s.size());
+  ASSERT_TRUE(Toolbox::IsAsciiString(s));
+  ASSERT_TRUE(Toolbox::IsAsciiString(s.c_str(), 10));
+  ASSERT_FALSE(Toolbox::IsAsciiString(s.c_str(), 11));  // Taking the trailing hidden '\0'
+
+  s[2] = '\0';
+  ASSERT_EQ(10u, s.size());
+  ASSERT_FALSE(Toolbox::IsAsciiString(s));
+}
+
+
+#if defined(__linux__)
 TEST(OrthancInitialization, AbsoluteDirectory)
 {
   ASSERT_EQ("/tmp/hello", Configuration::InterpretRelativePath("/tmp", "hello"));
@@ -524,11 +541,17 @@
   ASSERT_EQ(2047, StringToMetadata("Ceci est un test"));
 
   ASSERT_STREQ("Generic", EnumerationToString(StringToModalityManufacturer("Generic")));
+  ASSERT_STREQ("GenericNoWildcardInDates", EnumerationToString(StringToModalityManufacturer("GenericNoWildcardInDates")));
+  ASSERT_STREQ("GenericNoUniversalWildcard", EnumerationToString(StringToModalityManufacturer("GenericNoUniversalWildcard")));
   ASSERT_STREQ("StoreScp", EnumerationToString(StringToModalityManufacturer("StoreScp")));
   ASSERT_STREQ("ClearCanvas", EnumerationToString(StringToModalityManufacturer("ClearCanvas")));
-  ASSERT_STREQ("MedInria", EnumerationToString(StringToModalityManufacturer("MedInria")));
   ASSERT_STREQ("Dcm4Chee", EnumerationToString(StringToModalityManufacturer("Dcm4Chee")));
-  ASSERT_STREQ("SyngoVia", EnumerationToString(StringToModalityManufacturer("SyngoVia")));
+  ASSERT_STREQ("Vitrea", EnumerationToString(StringToModalityManufacturer("Vitrea")));
+  // backward compatibility tests (to remove once we make these manufacturer really obsolete)
+  ASSERT_STREQ("Generic", EnumerationToString(StringToModalityManufacturer("MedInria")));
+  ASSERT_STREQ("Generic", EnumerationToString(StringToModalityManufacturer("EFilm2")));
+  ASSERT_STREQ("GenericNoWildcardInDates", EnumerationToString(StringToModalityManufacturer("SyngoVia")));
+  ASSERT_STREQ("GenericNoWildcardInDates", EnumerationToString(StringToModalityManufacturer("AgfaImpax")));
 }
 
 
@@ -538,7 +561,7 @@
   std::string path;
 
   {
-    Toolbox::TemporaryFile tmp;
+    TemporaryFile tmp;
     path = tmp.GetPath();
 
     std::string s;
@@ -547,18 +570,28 @@
     s.append("World");
     ASSERT_EQ(11u, s.size());
 
-    Toolbox::WriteFile(s, path.c_str());
+    SystemToolbox::WriteFile(s, path.c_str());
 
     std::string t;
-    Toolbox::ReadFile(t, path.c_str());
+    SystemToolbox::ReadFile(t, path.c_str());
 
     ASSERT_EQ(11u, t.size());
     ASSERT_EQ(0, t[5]);
     ASSERT_EQ(0, memcmp(s.c_str(), t.c_str(), s.size()));
+
+    std::string h;
+    ASSERT_EQ(true, SystemToolbox::ReadHeader(h, path.c_str(), 1));
+    ASSERT_EQ(1u, h.size());
+    ASSERT_EQ('H', h[0]);
+    ASSERT_TRUE(SystemToolbox::ReadHeader(h, path.c_str(), 0));
+    ASSERT_EQ(0u, h.size());
+    ASSERT_FALSE(SystemToolbox::ReadHeader(h, path.c_str(), 32));
+    ASSERT_EQ(11u, h.size());
+    ASSERT_EQ(0, memcmp(s.c_str(), h.c_str(), s.size()));
   }
 
   std::string u;
-  ASSERT_THROW(Toolbox::ReadFile(u, path.c_str()), OrthancException);
+  ASSERT_THROW(SystemToolbox::ReadFile(u, path.c_str()), OrthancException);
 }
 
 
@@ -615,11 +648,48 @@
   ASSERT_EQ(ResourceType_Instance, StringToResourceType(EnumerationToString(ResourceType_Instance)));
 
   ASSERT_EQ(ImageFormat_Png, StringToImageFormat(EnumerationToString(ImageFormat_Png)));
+
+  ASSERT_EQ(PhotometricInterpretation_ARGB, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_ARGB)));
+  ASSERT_EQ(PhotometricInterpretation_CMYK, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_CMYK)));
+  ASSERT_EQ(PhotometricInterpretation_HSV, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_HSV)));
+  ASSERT_EQ(PhotometricInterpretation_Monochrome1, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_Monochrome1)));
+  ASSERT_EQ(PhotometricInterpretation_Monochrome2, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_Monochrome2)));
+  ASSERT_EQ(PhotometricInterpretation_Palette, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_Palette)));
+  ASSERT_EQ(PhotometricInterpretation_RGB, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_RGB)));
+  ASSERT_EQ(PhotometricInterpretation_YBRFull, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBRFull)));
+  ASSERT_EQ(PhotometricInterpretation_YBRFull422, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBRFull422)));
+  ASSERT_EQ(PhotometricInterpretation_YBRPartial420, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBRPartial420)));
+  ASSERT_EQ(PhotometricInterpretation_YBRPartial422, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBRPartial422)));
+  ASSERT_EQ(PhotometricInterpretation_YBR_ICT, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBR_ICT)));
+  ASSERT_EQ(PhotometricInterpretation_YBR_RCT, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBR_RCT)));
+
+  ASSERT_STREQ("Unknown", EnumerationToString(PhotometricInterpretation_Unknown));
+  ASSERT_THROW(StringToPhotometricInterpretation("Unknown"), OrthancException);
+
+  ASSERT_EQ(DicomVersion_2008, StringToDicomVersion(EnumerationToString(DicomVersion_2008)));
+  ASSERT_EQ(DicomVersion_2017c, StringToDicomVersion(EnumerationToString(DicomVersion_2017c)));
+
+  for (int i = static_cast<int>(ValueRepresentation_ApplicationEntity);
+       i < static_cast<int>(ValueRepresentation_NotSupported); i += 1)
+  {
+    ValueRepresentation vr = static_cast<ValueRepresentation>(i);
+    ASSERT_EQ(vr, StringToValueRepresentation(EnumerationToString(vr), true));
+  }
+
+  ASSERT_THROW(StringToValueRepresentation("nope", true), OrthancException);
+
+  ASSERT_EQ(JobState_Pending, StringToJobState(EnumerationToString(JobState_Pending)));
+  ASSERT_EQ(JobState_Running, StringToJobState(EnumerationToString(JobState_Running)));
+  ASSERT_EQ(JobState_Success, StringToJobState(EnumerationToString(JobState_Success)));
+  ASSERT_EQ(JobState_Failure, StringToJobState(EnumerationToString(JobState_Failure)));
+  ASSERT_EQ(JobState_Paused, StringToJobState(EnumerationToString(JobState_Paused)));
+  ASSERT_EQ(JobState_Retry, StringToJobState(EnumerationToString(JobState_Retry)));
+  ASSERT_THROW(StringToJobState("nope"), OrthancException);
 }
 
 
 
-#if defined(__linux)
+#if defined(__linux__) || defined(__OpenBSD__)
 #include <endian.h>
 #elif defined(__FreeBSD__)
 #include <machine/endian.h>
@@ -639,12 +709,24 @@
 #if defined(_WIN32) || defined(__APPLE__)
   ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness());
 
+  
+  /**
+   * FreeBSD.
+   **/
+  
+#elif defined(__FreeBSD__) || defined(__OpenBSD__)
+#  if _BYTE_ORDER == _BIG_ENDIAN
+   ASSERT_EQ(Endianness_Big, Toolbox::DetectEndianness());
+#  else // _LITTLE_ENDIAN
+   ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness());
+#  endif
+
 
   /**
    * Linux.
    **/
   
-#elif defined(__linux) || defined(__FreeBSD_kernel__)
+#elif defined(__linux__) || defined(__FreeBSD_kernel__)
 
 #if !defined(__BYTE_ORDER)
 #  error Support your platform here
@@ -656,25 +738,209 @@
   ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness());
 #  endif
 
-  
-  /**
-   * FreeBSD.
-   **/
-  
-#elif defined(__FreeBSD__)
-#  if _BYTE_ORDER == _BIG_ENDIAN
-   ASSERT_EQ(Endianness_Big, Toolbox::DetectEndianness());
-#  else // _LITTLE_ENDIAN
-   ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness());
-#  endif
-
 #else
 #error Support your platform here
 #endif
 }
 
 
-#if ORTHANC_PUGIXML_ENABLED == 1
+#include "../Core/Endianness.h"
+
+static void ASSERT_EQ16(uint16_t a, uint16_t b)
+{
+#ifdef __MINGW32__
+  // This cast solves a linking problem with MinGW
+  ASSERT_EQ(static_cast<unsigned int>(a), static_cast<unsigned int>(b));
+#else
+  ASSERT_EQ(a, b);
+#endif
+}
+
+static void ASSERT_NE16(uint16_t a, uint16_t b)
+{
+#ifdef __MINGW32__
+  // This cast solves a linking problem with MinGW
+  ASSERT_NE(static_cast<unsigned int>(a), static_cast<unsigned int>(b));
+#else
+  ASSERT_NE(a, b);
+#endif
+}
+
+static void ASSERT_EQ32(uint32_t a, uint32_t b)
+{
+#ifdef __MINGW32__
+  // This cast solves a linking problem with MinGW
+  ASSERT_EQ(static_cast<unsigned int>(a), static_cast<unsigned int>(b));
+#else
+  ASSERT_EQ(a, b);
+#endif
+}
+
+static void ASSERT_NE32(uint32_t a, uint32_t b)
+{
+#ifdef __MINGW32__
+  // This cast solves a linking problem with MinGW
+  ASSERT_NE(static_cast<unsigned int>(a), static_cast<unsigned int>(b));
+#else
+  ASSERT_NE(a, b);
+#endif
+}
+
+static void ASSERT_EQ64(uint64_t a, uint64_t b)
+{
+#ifdef __MINGW32__
+  // This cast solves a linking problem with MinGW
+  ASSERT_EQ(static_cast<unsigned int>(a), static_cast<unsigned int>(b));
+#else
+  ASSERT_EQ(a, b);
+#endif
+}
+
+static void ASSERT_NE64(uint64_t a, uint64_t b)
+{
+#ifdef __MINGW32__
+  // This cast solves a linking problem with MinGW
+  ASSERT_NE(static_cast<unsigned long long>(a), static_cast<unsigned long long>(b));
+#else
+  ASSERT_NE(a, b);
+#endif
+}
+
+
+
+TEST(Toolbox, EndiannessConversions16)
+{
+  Endianness e = Toolbox::DetectEndianness();
+
+  for (unsigned int i = 0; i < 65536; i += 17)
+  {
+    uint16_t v = static_cast<uint16_t>(i);
+    ASSERT_EQ16(v, be16toh(htobe16(v)));
+    ASSERT_EQ16(v, le16toh(htole16(v)));
+
+    const uint8_t* bytes = reinterpret_cast<const uint8_t*>(&v);
+    if (bytes[0] != bytes[1])
+    {
+      ASSERT_NE16(v, le16toh(htobe16(v)));
+      ASSERT_NE16(v, be16toh(htole16(v)));
+    }
+    else
+    {
+      ASSERT_EQ16(v, le16toh(htobe16(v)));
+      ASSERT_EQ16(v, be16toh(htole16(v)));
+    }
+
+    switch (e)
+    {
+      case Endianness_Little:
+        ASSERT_EQ16(v, htole16(v));
+        if (bytes[0] != bytes[1])
+        {
+          ASSERT_NE16(v, htobe16(v));
+        }
+        else
+        {
+          ASSERT_EQ16(v, htobe16(v));
+        }
+        break;
+
+      case Endianness_Big:
+        ASSERT_EQ16(v, htobe16(v));
+        if (bytes[0] != bytes[1])
+        {
+          ASSERT_NE16(v, htole16(v));
+        }
+        else
+        {
+          ASSERT_EQ16(v, htole16(v));
+        }
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+}
+
+
+TEST(Toolbox, EndiannessConversions32)
+{
+  const uint32_t v = 0xff010203u;
+  const uint32_t r = 0x030201ffu;
+  ASSERT_EQ32(v, be32toh(htobe32(v)));
+  ASSERT_EQ32(v, le32toh(htole32(v)));
+  ASSERT_NE32(v, be32toh(htole32(v)));
+  ASSERT_NE32(v, le32toh(htobe32(v)));
+
+  switch (Toolbox::DetectEndianness())
+  {
+    case Endianness_Little:
+      ASSERT_EQ32(r, htobe32(v));
+      ASSERT_EQ32(v, htole32(v));
+      ASSERT_EQ32(r, be32toh(v));
+      ASSERT_EQ32(v, le32toh(v));
+      break;
+
+    case Endianness_Big:
+      ASSERT_EQ32(v, htobe32(v));
+      ASSERT_EQ32(r, htole32(v));
+      ASSERT_EQ32(v, be32toh(v));
+      ASSERT_EQ32(r, le32toh(v));
+      break;
+
+    default:
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+}
+
+
+TEST(Toolbox, EndiannessConversions64)
+{
+  const uint64_t v = 0xff01020304050607LL;
+  const uint64_t r = 0x07060504030201ffLL;
+  ASSERT_EQ64(v, be64toh(htobe64(v)));
+  ASSERT_EQ64(v, le64toh(htole64(v)));
+  ASSERT_NE64(v, be64toh(htole64(v)));
+  ASSERT_NE64(v, le64toh(htobe64(v)));
+
+  switch (Toolbox::DetectEndianness())
+  {
+    case Endianness_Little:
+      ASSERT_EQ64(r, htobe64(v));
+      ASSERT_EQ64(v, htole64(v));
+      ASSERT_EQ64(r, be64toh(v));
+      ASSERT_EQ64(v, le64toh(v));
+      break;
+
+    case Endianness_Big:
+      ASSERT_EQ64(v, htobe64(v));
+      ASSERT_EQ64(r, htole64(v));
+      ASSERT_EQ64(v, be64toh(v));
+      ASSERT_EQ64(r, le64toh(v));
+      break;
+
+    default:
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+}
+
+
+TEST(Toolbox, Now)
+{
+  LOG(WARNING) << "Local time: " << SystemToolbox::GetNowIsoString(false);
+  LOG(WARNING) << "Universal time: " << SystemToolbox::GetNowIsoString(true);
+
+  std::string date, time;
+  SystemToolbox::GetNowDicom(date, time, false);
+  LOG(WARNING) << "Local DICOM time: [" << date << "] [" << time << "]";
+
+  SystemToolbox::GetNowDicom(date, time, true);
+  LOG(WARNING) << "Universal DICOM time: [" << date << "] [" << time << "]";
+}
+
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
 TEST(Toolbox, Xml)
 {
   Json::Value a;
@@ -699,7 +965,7 @@
   args[0] = "Hello";
   args[1] = "World";
 
-  Toolbox::ExecuteSystemCommand("echo", args);
+  SystemToolbox::ExecuteSystemCommand("echo", args);
 }
 #endif
 
@@ -729,12 +995,135 @@
 }
 
 
+TEST(Toolbox, UriEncode)
+{
+  std::string s;
+
+  // Unreserved characters must not be modified
+  std::string t = "aAzZ09.-~_";
+  Toolbox::UriEncode(s, t); 
+  ASSERT_EQ(t, s);
+
+  Toolbox::UriEncode(s, "!#$&'()*+,/:;=?@[]"); ASSERT_EQ("%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D", s);  
+  Toolbox::UriEncode(s, "%"); ASSERT_EQ("%25", s);
+
+  // Encode characters from UTF-8. This is the test string from the
+  // file "../Resources/EncodingTests.py"
+  Toolbox::UriEncode(s, "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xd0\x94\xce\x98\xc4\x9d\xd7\x93\xd8\xb5\xc4\xb7\xd1\x9b\xe0\xb9\x9b\xef\xbe\x88\xc4\xb0"); 
+  ASSERT_EQ("Test%C3%A9%C3%A4%C3%B6%C3%B2%D0%94%CE%98%C4%9D%D7%93%D8%B5%C4%B7%D1%9B%E0%B9%9B%EF%BE%88%C4%B0", s);
+}
+
+
+TEST(Toolbox, AccessJson)
+{
+  Json::Value v = Json::arrayValue;
+  ASSERT_EQ("nope", Toolbox::GetJsonStringField(v, "hello", "nope"));
+
+  v = Json::objectValue;
+  ASSERT_EQ("nope", Toolbox::GetJsonStringField(v, "hello", "nope"));
+  ASSERT_EQ(-10, Toolbox::GetJsonIntegerField(v, "hello", -10));
+  ASSERT_EQ(10u, Toolbox::GetJsonUnsignedIntegerField(v, "hello", 10));
+  ASSERT_TRUE(Toolbox::GetJsonBooleanField(v, "hello", true));
+
+  v["hello"] = "world";
+  ASSERT_EQ("world", Toolbox::GetJsonStringField(v, "hello", "nope"));
+  ASSERT_THROW(Toolbox::GetJsonIntegerField(v, "hello", -10), OrthancException);
+  ASSERT_THROW(Toolbox::GetJsonUnsignedIntegerField(v, "hello", 10), OrthancException);
+  ASSERT_THROW(Toolbox::GetJsonBooleanField(v, "hello", true), OrthancException);
+
+  v["hello"] = -42;
+  ASSERT_THROW(Toolbox::GetJsonStringField(v, "hello", "nope"), OrthancException);
+  ASSERT_EQ(-42, Toolbox::GetJsonIntegerField(v, "hello", -10));
+  ASSERT_THROW(Toolbox::GetJsonUnsignedIntegerField(v, "hello", 10), OrthancException);
+  ASSERT_THROW(Toolbox::GetJsonBooleanField(v, "hello", true), OrthancException);
+
+  v["hello"] = 42;
+  ASSERT_THROW(Toolbox::GetJsonStringField(v, "hello", "nope"), OrthancException);
+  ASSERT_EQ(42, Toolbox::GetJsonIntegerField(v, "hello", -10));
+  ASSERT_EQ(42u, Toolbox::GetJsonUnsignedIntegerField(v, "hello", 10));
+  ASSERT_THROW(Toolbox::GetJsonBooleanField(v, "hello", true), OrthancException);
+
+  v["hello"] = false;
+  ASSERT_THROW(Toolbox::GetJsonStringField(v, "hello", "nope"), OrthancException);
+  ASSERT_THROW(Toolbox::GetJsonIntegerField(v, "hello", -10), OrthancException);
+  ASSERT_THROW(Toolbox::GetJsonUnsignedIntegerField(v, "hello", 10), OrthancException);
+  ASSERT_FALSE(Toolbox::GetJsonBooleanField(v, "hello", true));
+}
+
+
+TEST(Toolbox, LinesIterator)
+{
+  std::string s;
+
+  {
+    std::string content;
+    Toolbox::LinesIterator it(content);
+    ASSERT_FALSE(it.GetLine(s));
+  }
+
+  {
+    std::string content = "\n\r";
+    Toolbox::LinesIterator it(content);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s);
+    ASSERT_FALSE(it.GetLine(s));
+  }
+  
+  {
+    std::string content = "\n Hello \n\nWorld\n\n";
+    Toolbox::LinesIterator it(content);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ(" Hello ", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("World", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s);
+    ASSERT_FALSE(it.GetLine(s)); it.Next();
+    ASSERT_FALSE(it.GetLine(s));
+  }
+
+  {
+    std::string content = "\r Hello \r\rWorld\r\r";
+    Toolbox::LinesIterator it(content);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ(" Hello ", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("World", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s);
+    ASSERT_FALSE(it.GetLine(s)); it.Next();
+    ASSERT_FALSE(it.GetLine(s));
+  }
+
+  {
+    std::string content = "\n\r Hello \n\r\n\rWorld\n\r\n\r";
+    Toolbox::LinesIterator it(content);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ(" Hello ", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("World", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s);
+    ASSERT_FALSE(it.GetLine(s)); it.Next();
+    ASSERT_FALSE(it.GetLine(s));
+  }
+
+  {
+    std::string content = "\r\n Hello \r\n\r\nWorld\r\n\r\n";
+    Toolbox::LinesIterator it(content);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ(" Hello ", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("World", s);
+    ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s);
+    ASSERT_FALSE(it.GetLine(s)); it.Next();
+    ASSERT_FALSE(it.GetLine(s));
+  }
+}
+
+
 int main(int argc, char **argv)
 {
   Logging::Initialize();
   Logging::EnableInfoLevel(true);
   Toolbox::DetectEndianness();
-  Toolbox::MakeDirectory("UnitTestsResults");
+  SystemToolbox::MakeDirectory("UnitTestsResults");
   OrthancInitialize();
 
   ::testing::InitGoogleTest(&argc, argv);
--- a/UnitTestsSources/VersionsTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/VersionsTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -43,8 +44,9 @@
 #include <sqlite3.h>
 #include <lua.h>
 #include <jpeglib.h>
+#include <iconv.h>
 
-#if ORTHANC_SSL_ENABLED == 1
+#if ORTHANC_ENABLE_SSL == 1
 #include <openssl/opensslv.h>
 #endif
 
@@ -97,23 +99,23 @@
 
 TEST(Versions, ZlibStatic)
 {
-  ASSERT_STREQ("1.2.7", zlibVersion());
+  ASSERT_STREQ("1.2.11", zlibVersion());
 }
 
 TEST(Versions, BoostStatic)
 {
-  ASSERT_STREQ("1_58", BOOST_LIB_VERSION);
+  ASSERT_STREQ("1_67", BOOST_LIB_VERSION);
 }
 
 TEST(Versions, CurlStatic)
 {
   curl_version_info_data* v = curl_version_info(CURLVERSION_NOW);
-  ASSERT_STREQ("7.44.0", v->version);
+  ASSERT_STREQ("7.57.0", v->version);
 }
 
 TEST(Versions, PngStatic)
 {
-  ASSERT_EQ(10512, png_access_version_number());
+  ASSERT_EQ(10512u, png_access_version_number());
   ASSERT_STREQ("1.5.12", PNG_LIBPNG_VER_STRING);
 }
 
@@ -130,7 +132,7 @@
   // Check that SSL support is enabled when required
   bool curlSupportsSsl = (vinfo->features & CURL_VERSION_SSL) != 0;
 
-#if ORTHANC_SSL_ENABLED == 0
+#if ORTHANC_ENABLE_SSL == 0
   ASSERT_FALSE(curlSupportsSsl);
 #else
   ASSERT_TRUE(curlSupportsSsl);
@@ -142,11 +144,18 @@
   ASSERT_STREQ("Lua 5.1.5", LUA_RELEASE);
 }
 
+TEST(Version, LibIconvStatic)
+{
+  static const int major = 1;
+  static const int minor = 15;  
+  ASSERT_EQ((major << 8) + minor, _LIBICONV_VERSION);
+}
 
-#if ORTHANC_SSL_ENABLED == 1
+
+#if ORTHANC_ENABLE_SSL == 1
 TEST(Version, OpenSslStatic)
 {
-  ASSERT_EQ(0x1000204fL /* openssl-1.0.2d */, OPENSSL_VERSION_NUMBER);
+  ASSERT_EQ(0x100020ffL /* openssl-1.0.2o */, OPENSSL_VERSION_NUMBER);
 }
 #endif
 
@@ -155,7 +164,13 @@
 
 TEST(Version, JsonCpp)
 {
-  ASSERT_STREQ("0.10.5", JSONCPP_VERSION_STRING);
+#if ORTHANC_LEGACY_JSONCPP == 1
+  ASSERT_STREQ("0.10.6", JSONCPP_VERSION_STRING);
+#elif ORTHANC_LEGACY_JSONCPP == 0
+  ASSERT_STREQ("1.8.4", JSONCPP_VERSION_STRING);
+#else
+#  error Macro ORTHANC_LEGACY_JSONCPP should be set
+#endif
 }
 
 #endif
--- a/UnitTestsSources/ZipTests.cpp	Thu Oct 29 11:25:45 2015 +0100
+++ b/UnitTestsSources/ZipTests.cpp	Fri Oct 12 15:18:10 2018 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as